├── .dockerignore ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── config.yml │ └── feature-request.md ├── PULL_REQUEST_TEMPLATE.md ├── SECURITY.md ├── config.yml ├── dependabot.yml ├── release.yml └── workflows │ ├── check.yaml │ └── release.yaml ├── .gitignore ├── .markdownlint.yaml ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── LICENSE ├── README.md ├── docs ├── _static │ └── custom.css ├── changelog.rst ├── changelog │ ├── examples.rst │ └── template.jinja2 ├── cli_interface.rst ├── conf.py ├── development.rst ├── extend.rst ├── index.rst ├── installation.rst ├── render_cli.py └── user_guide.rst ├── pyproject.toml ├── src └── virtualenv │ ├── __init__.py │ ├── __main__.py │ ├── activation │ ├── __init__.py │ ├── activator.py │ ├── bash │ │ ├── __init__.py │ │ └── activate.sh │ ├── batch │ │ ├── __init__.py │ │ ├── activate.bat │ │ ├── deactivate.bat │ │ └── pydoc.bat │ ├── cshell │ │ ├── __init__.py │ │ └── activate.csh │ ├── fish │ │ ├── __init__.py │ │ └── activate.fish │ ├── nushell │ │ ├── __init__.py │ │ └── activate.nu │ ├── powershell │ │ ├── __init__.py │ │ └── activate.ps1 │ ├── python │ │ ├── __init__.py │ │ └── activate_this.py │ └── via_template.py │ ├── app_data │ ├── __init__.py │ ├── base.py │ ├── na.py │ ├── read_only.py │ ├── via_disk_folder.py │ └── via_tempdir.py │ ├── config │ ├── __init__.py │ ├── cli │ │ ├── __init__.py │ │ └── parser.py │ ├── convert.py │ ├── env_var.py │ └── ini.py │ ├── create │ ├── __init__.py │ ├── creator.py │ ├── debug.py │ ├── describe.py │ ├── pyenv_cfg.py │ └── via_global_ref │ │ ├── __init__.py │ │ ├── _virtualenv.py │ │ ├── api.py │ │ ├── builtin │ │ ├── __init__.py │ │ ├── builtin_way.py │ │ ├── cpython │ │ │ ├── __init__.py │ │ │ ├── common.py │ │ │ ├── cpython3.py │ │ │ └── mac_os.py │ │ ├── graalpy │ │ │ └── __init__.py │ │ ├── pypy │ │ │ ├── __init__.py │ │ │ ├── common.py │ │ │ └── pypy3.py │ │ ├── ref.py │ │ └── via_global_self_do.py │ │ ├── store.py │ │ └── venv.py │ ├── discovery │ ├── __init__.py │ ├── builtin.py │ ├── cached_py_info.py │ ├── discover.py │ ├── py_info.py │ ├── py_spec.py │ └── windows │ │ ├── __init__.py │ │ └── pep514.py │ ├── info.py │ ├── report.py │ ├── run │ ├── __init__.py │ ├── plugin │ │ ├── __init__.py │ │ ├── activators.py │ │ ├── base.py │ │ ├── creators.py │ │ ├── discovery.py │ │ └── seeders.py │ └── session.py │ ├── seed │ ├── __init__.py │ ├── embed │ │ ├── __init__.py │ │ ├── base_embed.py │ │ ├── pip_invoke.py │ │ └── via_app_data │ │ │ ├── __init__.py │ │ │ ├── pip_install │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── copy.py │ │ │ └── symlink.py │ │ │ └── via_app_data.py │ ├── seeder.py │ └── wheels │ │ ├── __init__.py │ │ ├── acquire.py │ │ ├── bundle.py │ │ ├── embed │ │ ├── __init__.py │ │ ├── pip-25.0.1-py3-none-any.whl │ │ ├── pip-25.1.1-py3-none-any.whl │ │ ├── setuptools-75.3.2-py3-none-any.whl │ │ ├── setuptools-80.3.1-py3-none-any.whl │ │ └── wheel-0.45.1-py3-none-any.whl │ │ ├── periodic_update.py │ │ └── util.py │ └── util │ ├── __init__.py │ ├── error.py │ ├── lock.py │ ├── path │ ├── __init__.py │ ├── _permission.py │ ├── _sync.py │ └── _win.py │ ├── subprocess │ └── __init__.py │ └── zipapp.py ├── tasks ├── __main__zipapp.py ├── make_zipapp.py ├── pick_tox_env.py ├── release.py ├── update_embedded.py └── upgrade_wheels.py ├── tests ├── conftest.py ├── integration │ ├── test_cachedir_tag.py │ ├── test_run_int.py │ └── test_zipapp.py └── unit │ ├── activation │ ├── conftest.py │ ├── test_activation_support.py │ ├── test_activator.py │ ├── test_bash.py │ ├── test_batch.py │ ├── test_csh.py │ ├── test_fish.py │ ├── test_nushell.py │ ├── test_powershell.py │ └── test_python_activator.py │ ├── config │ ├── cli │ │ └── test_parser.py │ ├── test___main__.py │ ├── test_env_var.py │ └── test_ini.py │ ├── create │ ├── conftest.py │ ├── console_app │ │ ├── demo │ │ │ ├── __init__.py │ │ │ └── __main__.py │ │ ├── setup.cfg │ │ └── setup.py │ ├── test_creator.py │ ├── test_interpreters.py │ └── via_global_ref │ │ ├── builtin │ │ ├── conftest.py │ │ ├── cpython │ │ │ ├── cpython3_win_embed.json │ │ │ └── test_cpython3_win.py │ │ ├── pypy │ │ │ ├── deb_pypy37.json │ │ │ ├── deb_pypy38.json │ │ │ ├── portable_pypy38.json │ │ │ └── test_pypy3.py │ │ └── testing │ │ │ ├── __init__.py │ │ │ ├── helpers.py │ │ │ ├── path.py │ │ │ └── py_info.py │ │ ├── greet │ │ ├── greet2.c │ │ ├── greet3.c │ │ └── setup.py │ │ ├── test_api.py │ │ └── test_build_c_ext.py │ ├── discovery │ ├── py_info │ │ ├── test_py_info.py │ │ └── test_py_info_exe_based_of.py │ ├── test_discovery.py │ ├── test_py_spec.py │ └── windows │ │ ├── conftest.py │ │ ├── test_windows.py │ │ ├── test_windows_pep514.py │ │ └── winreg-mock-values.py │ ├── seed │ ├── embed │ │ ├── test_base_embed.py │ │ ├── test_bootstrap_link_via_app_data.py │ │ └── test_pip_invoke.py │ └── wheels │ │ ├── test_acquire.py │ │ ├── test_acquire_find_wheel.py │ │ ├── test_bundle.py │ │ ├── test_periodic_update.py │ │ └── test_wheels_util.py │ ├── test_run.py │ └── test_util.py └── tox.ini /.dockerignore: -------------------------------------------------------------------------------- 1 | .tox 2 | pip-wheel-metadata 3 | venv* 4 | *.pyz 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.bat text eol=crlf 2 | *.ps1 text eol=lf 3 | *.fish text eol=lf 4 | *.csh text eol=lf 5 | *.sh text eol=lf 6 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `virtualenv` 2 | 3 | Thank you for your interest in contributing to virtualenv! There are many ways to contribute, and we appreciate all of 4 | them. As a reminder, all contributors are expected to follow the [PSF Code of Conduct][coc]. 5 | 6 | [coc]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md 7 | 8 | ## Development Documentation 9 | 10 | Our [development documentation](https://virtualenv.pypa.io/en/latest/development.html#development) contains details on 11 | how to get started with contributing to `virtualenv`, and details of our development processes. 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | tidelift: "pypi/virtualenv" 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | **Issue** 10 | 11 | Describe what's the expected behavior and what you're observing. 12 | 13 | **Environment** 14 | 15 | Provide at least: 16 | 17 | - OS: 18 | - `pip list` of the host python where `virtualenv` is installed: 19 | 20 | ```console 21 | 22 | ``` 23 | 24 | **Output of the virtual environment creation** 25 | 26 | Make sure to run the creation with `-vvv --with-traceback`: 27 | 28 | ```console 29 | 30 | ``` 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser 2 | blank_issues_enabled: true # default 3 | contact_links: 4 | - name: "💬 pypa/virtualenv @ Discord" 5 | url: https://discord.gg/pypa 6 | about: Chat with the devs 7 | - name: 🤷💻🤦 Discourse 8 | url: https://discuss.python.org/c/packaging 9 | about: | 10 | Please ask typical Q&A here: general ideas for Python packaging, questions about structuring projects and so on 11 | - name: 📝 PSF Code of Conduct 12 | url: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md 13 | about: ❤ Be nice to other members of the community. ☮ Behave. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an enhancement for this project 4 | title: "" 5 | labels: enhancement 6 | assignees: "" 7 | --- 8 | 9 | **What's the problem this feature will solve?** 10 | 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | 17 | 18 | 19 | **Alternative Solutions** 20 | 21 | 23 | 24 | **Additional context** 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Thanks for contributing, make sure you address all the checklists (for details on how see [development documentation](https://virtualenv.pypa.io/en/latest/development.html#development)) 2 | 3 | - [ ] ran the linter to address style issues (`tox -e fix`) 4 | - [ ] wrote descriptive pull request text 5 | - [ ] ensured there are test(s) validating the fix 6 | - [ ] added news fragment in `docs/changelog` folder 7 | - [ ] updated/extended the documentation 8 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | --------- | ------------------ | 7 | | 20.15.1 + | :white_check_mark: | 8 | | < 20.15.1 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift 13 | will coordinate the fix and disclosure. 14 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | rtd: 2 | project: virtualenv 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - dependabot 5 | - pre-commit-ci 6 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release to PyPI 2 | on: 3 | push: 4 | tags: ["*"] 5 | 6 | env: 7 | dists-artifact-name: python-package-distributions 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - name: Install the latest version of uv 17 | uses: astral-sh/setup-uv@v4 18 | with: 19 | enable-cache: true 20 | cache-dependency-glob: "pyproject.toml" 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | - name: Build package 23 | run: uv build --python 3.13 --python-preference only-managed --sdist --wheel . --out-dir dist 24 | - name: Store the distribution packages 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: ${{ env.dists-artifact-name }} 28 | path: dist/* 29 | 30 | release: 31 | needs: 32 | - build 33 | runs-on: ubuntu-latest 34 | environment: 35 | name: release 36 | url: https://pypi.org/project/virtualenv/${{ github.ref_name }} 37 | permissions: 38 | id-token: write 39 | steps: 40 | - name: Download all the dists 41 | uses: actions/download-artifact@v4 42 | with: 43 | name: ${{ env.dists-artifact-name }} 44 | path: dist/ 45 | - name: Publish to PyPI 46 | uses: pypa/gh-action-pypi-publish@v1.12.3 47 | with: 48 | attestations: true 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # packaging 2 | *.egg-info 3 | build 4 | dist 5 | *.egg 6 | .eggs 7 | 8 | # python 9 | *.py[codz] 10 | *$py.class 11 | 12 | # tools 13 | .tox 14 | .*_cache 15 | .DS_Store 16 | 17 | # IDE 18 | .idea 19 | .vscode 20 | 21 | /docs/_draft.rst 22 | /pip-wheel-metadata 23 | /src/virtualenv/version.py 24 | /src/virtualenv/out 25 | .python-version 26 | 27 | *wheel-store* 28 | 29 | Dockerfile* 30 | .dockerignore 31 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | MD013: 2 | code_blocks: false 3 | headers: false 4 | line_length: 120 5 | tables: false 6 | 7 | MD046: 8 | style: fenced 9 | no-emphasis-as-header: false 10 | first-line-h1: false 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - id: trailing-whitespace 7 | - repo: https://github.com/python-jsonschema/check-jsonschema 8 | rev: 0.33.0 9 | hooks: 10 | - id: check-github-workflows 11 | args: ["--verbose"] 12 | - repo: https://github.com/codespell-project/codespell 13 | rev: v2.4.1 14 | hooks: 15 | - id: codespell 16 | args: ["--write-changes"] 17 | - repo: https://github.com/tox-dev/tox-ini-fmt 18 | rev: "1.5.0" 19 | hooks: 20 | - id: tox-ini-fmt 21 | args: ["-p", "fix"] 22 | - repo: https://github.com/tox-dev/pyproject-fmt 23 | rev: "v2.6.0" 24 | hooks: 25 | - id: pyproject-fmt 26 | - repo: https://github.com/astral-sh/ruff-pre-commit 27 | rev: "v0.11.11" 28 | hooks: 29 | - id: ruff-format 30 | - id: ruff 31 | args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] 32 | - repo: https://github.com/rbubley/mirrors-prettier 33 | rev: "v3.5.3" 34 | hooks: 35 | - id: prettier 36 | additional_dependencies: 37 | - prettier@3.3.3 38 | - "@prettier/plugin-xml@3.4.1" 39 | - repo: meta 40 | hooks: 41 | - id: check-hooks-apply 42 | - id: check-useless-excludes 43 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-22.04 4 | tools: 5 | python: "3" 6 | python: 7 | install: 8 | - method: pip 9 | path: . 10 | extra_requirements: 11 | - docs 12 | sphinx: 13 | builder: html 14 | configuration: docs/conf.py 15 | fail_on_warning: true 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-202x The virtualenv developers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # virtualenv 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/virtualenv?style=flat-square)](https://pypi.org/project/virtualenv) 4 | [![PyPI - Implementation](https://img.shields.io/pypi/implementation/virtualenv?style=flat-square)](https://pypi.org/project/virtualenv) 5 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/virtualenv?style=flat-square)](https://pypi.org/project/virtualenv) 6 | [![Documentation](https://readthedocs.org/projects/virtualenv/badge/?version=latest&style=flat-square)](http://virtualenv.pypa.io) 7 | [![Discord](https://img.shields.io/discord/803025117553754132)](https://discord.gg/pypa) 8 | [![Downloads](https://static.pepy.tech/badge/virtualenv/month)](https://pepy.tech/project/virtualenv) 9 | [![PyPI - License](https://img.shields.io/pypi/l/virtualenv?style=flat-square)](https://opensource.org/licenses/MIT) 10 | [![check](https://github.com/pypa/virtualenv/actions/workflows/check.yaml/badge.svg)](https://github.com/pypa/virtualenv/actions/workflows/check.yaml) 11 | 12 | A tool for creating isolated `virtual` python environments. 13 | 14 | - [Installation](https://virtualenv.pypa.io/en/latest/installation.html) 15 | - [Documentation](https://virtualenv.pypa.io) 16 | - [Changelog](https://virtualenv.pypa.io/en/latest/changelog.html) 17 | - [Issues](https://github.com/pypa/virtualenv/issues) 18 | - [PyPI](https://pypi.org/project/virtualenv) 19 | - [Github](https://github.com/pypa/virtualenv) 20 | 21 | ## Code of Conduct 22 | 23 | Everyone interacting in the virtualenv project's codebases, issue trackers, chat rooms, and mailing lists is expected to 24 | follow the [PSF Code of Conduct](https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md). 25 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | .wy-nav-content { 2 | padding: 1em; 3 | } 4 | 5 | #virtualenv img { 6 | margin-bottom: 6px; 7 | } 8 | 9 | /* Allow table content to wrap around */ 10 | .wy-table-responsive table th, 11 | .wy-table-responsive table td { 12 | /* !important because RTD has conflicting stylesheets */ 13 | white-space: normal !important; 14 | padding: 8px 6px !important; 15 | } 16 | 17 | .wy-table-responsive table { 18 | width: 100%; 19 | margin-left: 0 !important; 20 | } 21 | 22 | .rst-content table.docutils td ol { 23 | margin-bottom: 0; 24 | } 25 | 26 | .rst-content table.docutils td ul { 27 | margin-bottom: 0; 28 | } 29 | 30 | .rst-content table.docutils td p { 31 | margin-bottom: 0; 32 | } 33 | 34 | div[class*="highlight-"] { 35 | margin-bottom: 12px; 36 | } 37 | 38 | /* Tweak whitespace on the release history page */ 39 | #release-history p { 40 | margin-bottom: 0; 41 | margin-top: 0; 42 | } 43 | 44 | #release-history h3 { 45 | margin-bottom: 6px; 46 | } 47 | 48 | #release-history ul { 49 | margin-bottom: 12px; 50 | } 51 | 52 | #release-history ul ul { 53 | margin-bottom: 0; 54 | margin-top: 0; 55 | } 56 | 57 | #release-history h2 { 58 | margin-bottom: 12px; 59 | } 60 | 61 | /* Reduce whitespace on the inline-code snippets and add softer corners */ 62 | .rst-content code { 63 | padding: 2px 3px; 64 | border-radius: 3px; 65 | } 66 | -------------------------------------------------------------------------------- /docs/changelog/examples.rst: -------------------------------------------------------------------------------- 1 | .. examples for changelog entries adding to your Pull Requests 2 | 3 | file ``544.doc.rst``:: 4 | 5 | explain everything much better - by :user:`passionate_technicalwriter`. 6 | 7 | file ``544.feature.rst``:: 8 | 9 | ``tox --version`` now shows information about all registered plugins - by :user:`obestwalter`. 10 | 11 | 12 | file ``571.bugfix.rst``:: 13 | 14 | ``skip_install`` overrides ``usedevelop`` (``usedevelop`` is an option to choose the 15 | installation type if the package is installed and ``skip_install`` determines if it should be 16 | installed at all) - by :user:`ferdonline`. 17 | 18 | .. see pyproject.toml for all available categories 19 | -------------------------------------------------------------------------------- /docs/changelog/template.jinja2: -------------------------------------------------------------------------------- 1 | {% set top_underline = underlines[0] %} 2 | {% if versiondata.name %} 3 | v{{ versiondata.version }} ({{ versiondata.date }}) 4 | {{ top_underline * ((versiondata.version + versiondata.date)|length + 4)}} 5 | {% else %} 6 | {{ versiondata.version }} ({{ versiondata.date }}) 7 | {{ top_underline * ((versiondata.version + versiondata.date)|length + 3)}} 8 | {% endif %} 9 | 10 | {% for section, _ in sections.items() %} 11 | {% set underline = underlines[1] %} 12 | {% if sections[section] %} 13 | {% for category, val in definitions.items() if category in sections[section]%} 14 | {{ definitions[category]['name'] }} - {{ versiondata.version }} 15 | {{ underline * ((definitions[category]['name'] + versiondata.version)|length + 3)}} 16 | {% if definitions[category]['showcontent'] %} 17 | {% for text, values in sections[section][category].items() %} 18 | - {{ text }} ({{ values|join(', ') }}) 19 | {% endfor %} 20 | 21 | {% else %} 22 | - {{ sections[section][category]['']|join(', ') }} 23 | 24 | {% endif %} 25 | {% if sections[section][category]|length == 0 %} 26 | No significant changes. 27 | 28 | {% else %} 29 | {% endif %} 30 | {% endfor %} 31 | {% else %} 32 | No significant changes. 33 | 34 | 35 | {% endif %} 36 | {% endfor %} 37 | -------------------------------------------------------------------------------- /docs/cli_interface.rst: -------------------------------------------------------------------------------- 1 | CLI interface 2 | ============= 3 | 4 | .. _cli_flags: 5 | 6 | CLI flags 7 | ~~~~~~~~~ 8 | 9 | ``virtualenv`` is primarily a command line application. 10 | 11 | It modifies the environment variables in a shell to create an isolated Python environment, so you'll need to have a 12 | shell to run it. You can type in ``virtualenv`` (name of the application) followed by flags that control its 13 | behavior. All options have sensible defaults, and there's one required argument: the name/path of the virtual 14 | environment to create. The default values for the command line options can be overridden via the 15 | :ref:`conf_file` or :ref:`env_vars`. Environment variables takes priority over the configuration file values 16 | (``--help`` will show if a default comes from the environment variable as the help message will end in this case 17 | with environment variables or the configuration file). 18 | 19 | The options that can be passed to virtualenv, along with their default values and a short description are listed below. 20 | 21 | :command:`virtualenv [OPTIONS]` 22 | 23 | .. table_cli:: 24 | :module: virtualenv.run 25 | :func: build_parser_only 26 | 27 | Defaults 28 | ~~~~~~~~ 29 | 30 | .. _conf_file: 31 | 32 | Configuration file 33 | ^^^^^^^^^^^^^^^^^^ 34 | 35 | Unless ``VIRTUALENV_CONFIG_FILE`` is set, virtualenv looks for a standard ``virtualenv.ini`` configuration file. 36 | The exact location depends on the operating system you're using, as determined by :pypi:`platformdirs` application 37 | configuration definition. It can be overridden by setting the ``VIRTUALENV_CONFIG_FILE`` environment variable. 38 | The configuration file location is printed as at the end of the output when ``--help`` is passed. 39 | 40 | The keys of the settings are derived from the command line option (left strip the ``-`` characters, and replace ``-`` 41 | with ``_``). Where multiple flags are available first found wins (where order is as it shows up under the ``--help``). 42 | 43 | For example, :option:`--python ` would be specified as: 44 | 45 | .. code-block:: ini 46 | 47 | [virtualenv] 48 | python = /opt/python-3.8/bin/python 49 | 50 | Options that take multiple values, like :option:`extra-search-dir` can be specified as: 51 | 52 | .. code-block:: ini 53 | 54 | [virtualenv] 55 | extra_search_dir = 56 | /path/to/dists 57 | /path/to/other/dists 58 | 59 | .. _env_vars: 60 | 61 | Environment Variables 62 | ^^^^^^^^^^^^^^^^^^^^^ 63 | 64 | Default values may be also specified via environment variables. The keys of the settings are derived from the 65 | command line option (left strip the ``-`` characters, and replace ``-`` with ``_``, finally capitalize the name). Where 66 | multiple flags are available first found wins (where order is as it shows up under the ``--help``). 67 | 68 | For example, to use a custom Python binary, instead of the one virtualenv is run with, you can set the environment 69 | variable ``VIRTUALENV_PYTHON`` like: 70 | 71 | .. code-block:: console 72 | 73 | env VIRTUALENV_PYTHON=/opt/python-3.8/bin/python virtualenv 74 | 75 | Where the option accepts multiple values, for example for :option:`python` or 76 | :option:`extra-search-dir`, the values can be separated either by literal 77 | newlines or commas. Newlines and commas can not be mixed and if both are 78 | present only the newline is used for separating values. Examples for multiple 79 | values: 80 | 81 | 82 | .. code-block:: console 83 | 84 | env VIRTUALENV_PYTHON=/opt/python-3.8/bin/python,python3.8 virtualenv 85 | env VIRTUALENV_EXTRA_SEARCH_DIR=/path/to/dists\n/path/to/other/dists virtualenv 86 | 87 | The equivalent CLI-flags based invocation for the above examples would be: 88 | 89 | .. code-block:: console 90 | 91 | virtualenv --python=/opt/python-3.8/bin/python --python=python3.8 92 | virtualenv --extra-search-dir=/path/to/dists --extra-search-dir=/path/to/other/dists 93 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import subprocess 4 | import sys 5 | from datetime import datetime, timezone 6 | from pathlib import Path 7 | 8 | from virtualenv.version import __version__ 9 | 10 | company = "PyPA" 11 | name = "virtualenv" 12 | version = ".".join(__version__.split(".")[:2]) 13 | release = __version__ 14 | copyright = f"2007-{datetime.now(tz=timezone.utc).year}, {company}, PyPA" # noqa: A001 15 | 16 | extensions = [ 17 | "sphinx.ext.autodoc", 18 | "sphinx.ext.autosectionlabel", 19 | "sphinx.ext.extlinks", 20 | ] 21 | 22 | templates_path = [] 23 | unused_docs = [] 24 | source_suffix = ".rst" 25 | exclude_patterns = ["_build", "changelog/*", "_draft.rst"] 26 | 27 | main_doc = "index" 28 | pygments_style = "default" 29 | always_document_param_types = True 30 | project = name 31 | today_fmt = "%B %d, %Y" 32 | 33 | html_theme = "furo" 34 | html_title, html_last_updated_fmt = project, datetime.now(tz=timezone.utc).isoformat() 35 | pygments_style, pygments_dark_style = "sphinx", "monokai" 36 | html_static_path, html_css_files = ["_static"], ["custom.css"] 37 | 38 | autoclass_content = "both" # Include __init__ in class documentation 39 | autodoc_member_order = "bysource" 40 | autosectionlabel_prefix_document = True 41 | 42 | extlinks = { 43 | "issue": ("https://github.com/pypa/virtualenv/issues/%s", "#%s"), 44 | "pull": ("https://github.com/pypa/virtualenv/pull/%s", "PR #%s"), 45 | "user": ("https://github.com/%s", "@%s"), 46 | "pypi": ("https://pypi.org/project/%s", "%s"), 47 | } 48 | 49 | 50 | def setup(app): 51 | here = Path(__file__).parent 52 | root, exe = here.parent, Path(sys.executable) 53 | towncrier = exe.with_name(f"towncrier{exe.suffix}") 54 | cmd = [str(towncrier), "build", "--draft", "--version", "NEXT"] 55 | new = subprocess.check_output(cmd, cwd=root, text=True, stderr=subprocess.DEVNULL, encoding="UTF-8") 56 | (root / "docs" / "_draft.rst").write_text("" if "No significant changes" in new else new, encoding="UTF-8") 57 | 58 | # the CLI arguments are dynamically generated 59 | doc_tree = Path(app.doctreedir) 60 | cli_interface_doctree = doc_tree / "cli_interface.doctree" 61 | if cli_interface_doctree.exists(): 62 | cli_interface_doctree.unlink() 63 | 64 | here = Path(__file__).parent 65 | if str(here) not in sys.path: 66 | sys.path.append(str(here)) 67 | 68 | # noinspection PyUnresolvedReferences 69 | from render_cli import CliTable, literal_data # noqa: PLC0415 70 | 71 | app.add_css_file("custom.css") 72 | app.add_directive(CliTable.name, CliTable) 73 | app.add_role("literal_data", literal_data) 74 | -------------------------------------------------------------------------------- /docs/extend.rst: -------------------------------------------------------------------------------- 1 | Extend functionality 2 | ==================== 3 | 4 | ``virtualenv`` allows one to extend the builtin functionality via a plugin system. To add a plugin you need to: 5 | 6 | - write a python file containing the plugin code which follows our expected interface, 7 | - package it as a python library, 8 | - install it alongside the virtual environment. 9 | 10 | Python discovery 11 | ---------------- 12 | 13 | The python discovery mechanism is a component that needs to answer the following question: based on some type of user 14 | input give me a Python interpreter on the machine that matches that. The builtin interpreter tries to discover 15 | an installed Python interpreter (based on PEP-515 and ``PATH`` discovery) on the users machine where the user input is a 16 | python specification. An alternative such discovery mechanism for example would be to use the popular 17 | `pyenv `_ project to discover, and if not present install the requested Python 18 | interpreter. Python discovery mechanisms must be registered under key ``virtualenv.discovery``, and the plugin must 19 | implement :class:`virtualenv.discovery.discover.Discover`: 20 | 21 | .. code-block:: ini 22 | 23 | virtualenv.discovery = 24 | pyenv = virtualenv_pyenv.discovery:PyEnvDiscovery 25 | 26 | 27 | .. currentmodule:: virtualenv.discovery.discover 28 | 29 | .. autoclass:: Discover 30 | :undoc-members: 31 | :members: 32 | 33 | 34 | Creators 35 | -------- 36 | Creators are what actually perform the creation of a virtual environment. The builtin virtual environment creators 37 | all achieve this by referencing a global install; but would be just as valid for a creator to install a brand new 38 | entire python under the target path; or one could add additional creators that can create virtual environments for other 39 | python implementations, such as IronPython. They must be registered under and entry point with key 40 | ``virtualenv.create`` , and the class must implement :class:`virtualenv.create.creator.Creator`: 41 | 42 | .. code-block:: ini 43 | 44 | virtualenv.create = 45 | cpython3-posix = virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Posix 46 | 47 | .. currentmodule:: virtualenv.create.creator 48 | 49 | .. autoclass:: Creator 50 | :undoc-members: 51 | :members: 52 | :exclude-members: run, set_pyenv_cfg, debug_script, debug_script, validate_dest, debug 53 | 54 | 55 | Seed mechanism 56 | -------------- 57 | 58 | Seeders are what given a virtual environment will install somehow some seed packages into it. They must be registered 59 | under and entry point with key ``virtualenv.seed`` , and the class must implement 60 | :class:`virtualenv.seed.seeder.Seeder`: 61 | 62 | .. code-block:: ini 63 | 64 | virtualenv.seed = 65 | db = virtualenv.seed.fromDb:InstallFromDb 66 | 67 | .. currentmodule:: virtualenv.seed.seeder 68 | 69 | .. autoclass:: Seeder 70 | :undoc-members: 71 | :members: 72 | 73 | Activation scripts 74 | ------------------ 75 | If you want add an activator for a new shell you can do this by implementing a new activator. They must be registered 76 | under an entry point with key ``virtualenv.activate`` , and the class must implement 77 | :class:`virtualenv.activation.activator.Activator`: 78 | 79 | .. code-block:: ini 80 | 81 | virtualenv.activate = 82 | bash = virtualenv.activation.bash:BashActivator 83 | 84 | .. currentmodule:: virtualenv.activation.activator 85 | 86 | .. autoclass:: Activator 87 | :undoc-members: 88 | :members: 89 | -------------------------------------------------------------------------------- /src/virtualenv/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .run import cli_run, session_via_cli 4 | from .version import __version__ 5 | 6 | __all__ = [ 7 | "__version__", 8 | "cli_run", 9 | "session_via_cli", 10 | ] 11 | -------------------------------------------------------------------------------- /src/virtualenv/__main__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os 5 | import sys 6 | from timeit import default_timer 7 | 8 | LOGGER = logging.getLogger(__name__) 9 | 10 | 11 | def run(args=None, options=None, env=None): 12 | env = os.environ if env is None else env 13 | start = default_timer() 14 | from virtualenv.run import cli_run # noqa: PLC0415 15 | from virtualenv.util.error import ProcessCallFailedError # noqa: PLC0415 16 | 17 | if args is None: 18 | args = sys.argv[1:] 19 | try: 20 | session = cli_run(args, options, env) 21 | LOGGER.warning(LogSession(session, start)) 22 | except ProcessCallFailedError as exception: 23 | print(f"subprocess call failed for {exception.cmd} with code {exception.code}") # noqa: T201 24 | print(exception.out, file=sys.stdout, end="") # noqa: T201 25 | print(exception.err, file=sys.stderr, end="") # noqa: T201 26 | raise SystemExit(exception.code) # noqa: B904 27 | 28 | 29 | class LogSession: 30 | def __init__(self, session, start) -> None: 31 | self.session = session 32 | self.start = start 33 | 34 | def __str__(self) -> str: 35 | spec = self.session.creator.interpreter.spec 36 | elapsed = (default_timer() - self.start) * 1000 37 | lines = [ 38 | f"created virtual environment {spec} in {elapsed:.0f}ms", 39 | f" creator {self.session.creator!s}", 40 | ] 41 | if self.session.seeder.enabled: 42 | lines.append(f" seeder {self.session.seeder!s}") 43 | path = self.session.creator.purelib.iterdir() 44 | packages = sorted("==".join(i.stem.split("-")) for i in path if i.suffix == ".dist-info") 45 | lines.append(f" added seed packages: {', '.join(packages)}") 46 | 47 | if self.session.activators: 48 | lines.append(f" activators {','.join(i.__class__.__name__ for i in self.session.activators)}") 49 | return "\n".join(lines) 50 | 51 | 52 | def run_with_catch(args=None, env=None): 53 | from virtualenv.config.cli.parser import VirtualEnvOptions # noqa: PLC0415 54 | 55 | env = os.environ if env is None else env 56 | options = VirtualEnvOptions() 57 | try: 58 | run(args, options, env) 59 | except (KeyboardInterrupt, SystemExit, Exception) as exception: 60 | try: 61 | if getattr(options, "with_traceback", False): 62 | raise 63 | if not (isinstance(exception, SystemExit) and exception.code == 0): 64 | LOGGER.error("%s: %s", type(exception).__name__, exception) # noqa: TRY400 65 | code = exception.code if isinstance(exception, SystemExit) else 1 66 | sys.exit(code) 67 | finally: 68 | for handler in LOGGER.handlers: # force flush of log messages before the trace is printed 69 | handler.flush() 70 | 71 | 72 | if __name__ == "__main__": # pragma: no cov 73 | run_with_catch() # pragma: no cov 74 | -------------------------------------------------------------------------------- /src/virtualenv/activation/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .bash import BashActivator 4 | from .batch import BatchActivator 5 | from .cshell import CShellActivator 6 | from .fish import FishActivator 7 | from .nushell import NushellActivator 8 | from .powershell import PowerShellActivator 9 | from .python import PythonActivator 10 | 11 | __all__ = [ 12 | "BashActivator", 13 | "BatchActivator", 14 | "CShellActivator", 15 | "FishActivator", 16 | "NushellActivator", 17 | "PowerShellActivator", 18 | "PythonActivator", 19 | ] 20 | -------------------------------------------------------------------------------- /src/virtualenv/activation/activator.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from abc import ABC, abstractmethod 5 | 6 | 7 | class Activator(ABC): 8 | """Generates activate script for the virtual environment.""" 9 | 10 | def __init__(self, options) -> None: 11 | """ 12 | Create a new activator generator. 13 | 14 | :param options: the parsed options as defined within :meth:`add_parser_arguments` 15 | """ 16 | self.flag_prompt = os.path.basename(os.getcwd()) if options.prompt == "." else options.prompt 17 | 18 | @classmethod 19 | def supports(cls, interpreter): # noqa: ARG003 20 | """ 21 | Check if the activation script is supported in the given interpreter. 22 | 23 | :param interpreter: the interpreter we need to support 24 | :return: ``True`` if supported, ``False`` otherwise 25 | """ 26 | return True 27 | 28 | @classmethod # noqa: B027 29 | def add_parser_arguments(cls, parser, interpreter): 30 | """ 31 | Add CLI arguments for this activation script. 32 | 33 | :param parser: the CLI parser 34 | :param interpreter: the interpreter this virtual environment is based of 35 | """ 36 | 37 | @abstractmethod 38 | def generate(self, creator): 39 | """ 40 | Generate activate script for the given creator. 41 | 42 | :param creator: the creator (based of :class:`virtualenv.create.creator.Creator`) we used to create this \ 43 | virtual environment 44 | """ 45 | raise NotImplementedError 46 | 47 | 48 | __all__ = [ 49 | "Activator", 50 | ] 51 | -------------------------------------------------------------------------------- /src/virtualenv/activation/bash/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from virtualenv.activation.via_template import ViaTemplateActivator 6 | 7 | 8 | class BashActivator(ViaTemplateActivator): 9 | def templates(self): 10 | yield "activate.sh" 11 | 12 | def as_name(self, template): 13 | return Path(template).stem 14 | 15 | 16 | __all__ = [ 17 | "BashActivator", 18 | ] 19 | -------------------------------------------------------------------------------- /src/virtualenv/activation/bash/activate.sh: -------------------------------------------------------------------------------- 1 | # This file must be used with "source bin/activate" *from bash* 2 | # you cannot run it directly 3 | 4 | 5 | if [ "${BASH_SOURCE-}" = "$0" ]; then 6 | echo "You must source this script: \$ source $0" >&2 7 | exit 33 8 | fi 9 | 10 | deactivate () { 11 | unset -f pydoc >/dev/null 2>&1 || true 12 | 13 | # reset old environment variables 14 | # ! [ -z ${VAR+_} ] returns true if VAR is declared at all 15 | if ! [ -z "${_OLD_VIRTUAL_PATH:+_}" ] ; then 16 | PATH="$_OLD_VIRTUAL_PATH" 17 | export PATH 18 | unset _OLD_VIRTUAL_PATH 19 | fi 20 | if ! [ -z "${_OLD_VIRTUAL_PYTHONHOME+_}" ] ; then 21 | PYTHONHOME="$_OLD_VIRTUAL_PYTHONHOME" 22 | export PYTHONHOME 23 | unset _OLD_VIRTUAL_PYTHONHOME 24 | fi 25 | 26 | # The hash command must be called to get it to forget past 27 | # commands. Without forgetting past commands the $PATH changes 28 | # we made may not be respected 29 | hash -r 2>/dev/null 30 | 31 | if ! [ -z "${_OLD_VIRTUAL_PS1+_}" ] ; then 32 | PS1="$_OLD_VIRTUAL_PS1" 33 | export PS1 34 | unset _OLD_VIRTUAL_PS1 35 | fi 36 | 37 | unset VIRTUAL_ENV 38 | unset VIRTUAL_ENV_PROMPT 39 | if [ ! "${1-}" = "nondestructive" ] ; then 40 | # Self destruct! 41 | unset -f deactivate 42 | fi 43 | } 44 | 45 | # unset irrelevant variables 46 | deactivate nondestructive 47 | 48 | VIRTUAL_ENV=__VIRTUAL_ENV__ 49 | if ([ "$OSTYPE" = "cygwin" ] || [ "$OSTYPE" = "msys" ]) && $(command -v cygpath &> /dev/null) ; then 50 | VIRTUAL_ENV=$(cygpath -u "$VIRTUAL_ENV") 51 | fi 52 | export VIRTUAL_ENV 53 | 54 | _OLD_VIRTUAL_PATH="$PATH" 55 | PATH="$VIRTUAL_ENV/"__BIN_NAME__":$PATH" 56 | export PATH 57 | 58 | if [ "x"__VIRTUAL_PROMPT__ != x ] ; then 59 | VIRTUAL_ENV_PROMPT=__VIRTUAL_PROMPT__ 60 | else 61 | VIRTUAL_ENV_PROMPT=$(basename "$VIRTUAL_ENV") 62 | fi 63 | export VIRTUAL_ENV_PROMPT 64 | 65 | # unset PYTHONHOME if set 66 | if ! [ -z "${PYTHONHOME+_}" ] ; then 67 | _OLD_VIRTUAL_PYTHONHOME="$PYTHONHOME" 68 | unset PYTHONHOME 69 | fi 70 | 71 | if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then 72 | _OLD_VIRTUAL_PS1="${PS1-}" 73 | PS1="(${VIRTUAL_ENV_PROMPT}) ${PS1-}" 74 | export PS1 75 | fi 76 | 77 | # Make sure to unalias pydoc if it's already there 78 | alias pydoc 2>/dev/null >/dev/null && unalias pydoc || true 79 | 80 | pydoc () { 81 | python -m pydoc "$@" 82 | } 83 | 84 | # The hash command must be called to get it to forget past 85 | # commands. Without forgetting past commands the $PATH changes 86 | # we made may not be respected 87 | hash -r 2>/dev/null || true 88 | -------------------------------------------------------------------------------- /src/virtualenv/activation/batch/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | 5 | from virtualenv.activation.via_template import ViaTemplateActivator 6 | 7 | 8 | class BatchActivator(ViaTemplateActivator): 9 | @classmethod 10 | def supports(cls, interpreter): 11 | return interpreter.os == "nt" 12 | 13 | def templates(self): 14 | yield "activate.bat" 15 | yield "deactivate.bat" 16 | yield "pydoc.bat" 17 | 18 | @staticmethod 19 | def quote(string): 20 | return string 21 | 22 | def instantiate_template(self, replacements, template, creator): 23 | # ensure the text has all newlines as \r\n - required by batch 24 | base = super().instantiate_template(replacements, template, creator) 25 | return base.replace(os.linesep, "\n").replace("\n", os.linesep) 26 | 27 | 28 | __all__ = [ 29 | "BatchActivator", 30 | ] 31 | -------------------------------------------------------------------------------- /src/virtualenv/activation/batch/activate.bat: -------------------------------------------------------------------------------- 1 | @REM This file is UTF-8 encoded, so we need to update the current code page while executing it 2 | @for /f "tokens=2 delims=:." %%a in ('"%SystemRoot%\System32\chcp.com"') do @set _OLD_CODEPAGE=%%a 3 | 4 | @if defined _OLD_CODEPAGE ( 5 | "%SystemRoot%\System32\chcp.com" 65001 > nul 6 | ) 7 | 8 | @set "VIRTUAL_ENV=__VIRTUAL_ENV__" 9 | 10 | @set "VIRTUAL_ENV_PROMPT=__VIRTUAL_PROMPT__" 11 | @if NOT DEFINED VIRTUAL_ENV_PROMPT ( 12 | @for %%d in ("%VIRTUAL_ENV%") do @set "VIRTUAL_ENV_PROMPT=%%~nxd" 13 | ) 14 | 15 | @if defined _OLD_VIRTUAL_PROMPT ( 16 | @set "PROMPT=%_OLD_VIRTUAL_PROMPT%" 17 | ) else ( 18 | @if not defined PROMPT ( 19 | @set "PROMPT=$P$G" 20 | ) 21 | @if not defined VIRTUAL_ENV_DISABLE_PROMPT ( 22 | @set "_OLD_VIRTUAL_PROMPT=%PROMPT%" 23 | ) 24 | ) 25 | @if not defined VIRTUAL_ENV_DISABLE_PROMPT ( 26 | @set "PROMPT=(%VIRTUAL_ENV_PROMPT%) %PROMPT%" 27 | ) 28 | 29 | @REM Don't use () to avoid problems with them in %PATH% 30 | @if defined _OLD_VIRTUAL_PYTHONHOME @goto ENDIFVHOME 31 | @set "_OLD_VIRTUAL_PYTHONHOME=%PYTHONHOME%" 32 | :ENDIFVHOME 33 | 34 | @set PYTHONHOME= 35 | 36 | @REM if defined _OLD_VIRTUAL_PATH ( 37 | @if not defined _OLD_VIRTUAL_PATH @goto ENDIFVPATH1 38 | @set "PATH=%_OLD_VIRTUAL_PATH%" 39 | :ENDIFVPATH1 40 | @REM ) else ( 41 | @if defined _OLD_VIRTUAL_PATH @goto ENDIFVPATH2 42 | @set "_OLD_VIRTUAL_PATH=%PATH%" 43 | :ENDIFVPATH2 44 | 45 | @set "PATH=%VIRTUAL_ENV%\__BIN_NAME__;%PATH%" 46 | 47 | @if defined _OLD_CODEPAGE ( 48 | "%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul 49 | @set _OLD_CODEPAGE= 50 | ) 51 | -------------------------------------------------------------------------------- /src/virtualenv/activation/batch/deactivate.bat: -------------------------------------------------------------------------------- 1 | @set VIRTUAL_ENV= 2 | @set VIRTUAL_ENV_PROMPT= 3 | 4 | @REM Don't use () to avoid problems with them in %PATH% 5 | @if not defined _OLD_VIRTUAL_PROMPT @goto ENDIFVPROMPT 6 | @set "PROMPT=%_OLD_VIRTUAL_PROMPT%" 7 | @set _OLD_VIRTUAL_PROMPT= 8 | :ENDIFVPROMPT 9 | 10 | @if not defined _OLD_VIRTUAL_PYTHONHOME @goto ENDIFVHOME 11 | @set "PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%" 12 | @set _OLD_VIRTUAL_PYTHONHOME= 13 | :ENDIFVHOME 14 | 15 | @if not defined _OLD_VIRTUAL_PATH @goto ENDIFVPATH 16 | @set "PATH=%_OLD_VIRTUAL_PATH%" 17 | @set _OLD_VIRTUAL_PATH= 18 | :ENDIFVPATH 19 | -------------------------------------------------------------------------------- /src/virtualenv/activation/batch/pydoc.bat: -------------------------------------------------------------------------------- 1 | python.exe -m pydoc %* 2 | -------------------------------------------------------------------------------- /src/virtualenv/activation/cshell/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from virtualenv.activation.via_template import ViaTemplateActivator 4 | 5 | 6 | class CShellActivator(ViaTemplateActivator): 7 | @classmethod 8 | def supports(cls, interpreter): 9 | return interpreter.os != "nt" 10 | 11 | def templates(self): 12 | yield "activate.csh" 13 | 14 | 15 | __all__ = [ 16 | "CShellActivator", 17 | ] 18 | -------------------------------------------------------------------------------- /src/virtualenv/activation/cshell/activate.csh: -------------------------------------------------------------------------------- 1 | # This file must be used with "source bin/activate.csh" *from csh*. 2 | # You cannot run it directly. 3 | # Created by Davide Di Blasi . 4 | 5 | set newline='\ 6 | ' 7 | 8 | alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH:q" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT:q" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate && unalias pydoc' 9 | 10 | # Unset irrelevant variables. 11 | deactivate nondestructive 12 | 13 | setenv VIRTUAL_ENV __VIRTUAL_ENV__ 14 | 15 | set _OLD_VIRTUAL_PATH="$PATH:q" 16 | setenv PATH "$VIRTUAL_ENV:q/"__BIN_NAME__":$PATH:q" 17 | 18 | 19 | 20 | if (__VIRTUAL_PROMPT__ != "") then 21 | setenv VIRTUAL_ENV_PROMPT __VIRTUAL_PROMPT__ 22 | else 23 | setenv VIRTUAL_ENV_PROMPT "$VIRTUAL_ENV:t:q" 24 | endif 25 | 26 | if ( $?VIRTUAL_ENV_DISABLE_PROMPT ) then 27 | if ( $VIRTUAL_ENV_DISABLE_PROMPT == "" ) then 28 | set do_prompt = "1" 29 | else 30 | set do_prompt = "0" 31 | endif 32 | else 33 | set do_prompt = "1" 34 | endif 35 | 36 | if ( $do_prompt == "1" ) then 37 | # Could be in a non-interactive environment, 38 | # in which case, $prompt is undefined and we wouldn't 39 | # care about the prompt anyway. 40 | if ( $?prompt ) then 41 | set _OLD_VIRTUAL_PROMPT="$prompt:q" 42 | if ( "$prompt:q" =~ *"$newline:q"* ) then 43 | : 44 | else 45 | set prompt = '('"$VIRTUAL_ENV_PROMPT:q"') '"$prompt:q" 46 | endif 47 | endif 48 | endif 49 | 50 | unset env_name 51 | unset do_prompt 52 | 53 | alias pydoc python -m pydoc 54 | 55 | rehash 56 | -------------------------------------------------------------------------------- /src/virtualenv/activation/fish/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from virtualenv.activation.via_template import ViaTemplateActivator 4 | 5 | 6 | class FishActivator(ViaTemplateActivator): 7 | def templates(self): 8 | yield "activate.fish" 9 | 10 | 11 | __all__ = [ 12 | "FishActivator", 13 | ] 14 | -------------------------------------------------------------------------------- /src/virtualenv/activation/fish/activate.fish: -------------------------------------------------------------------------------- 1 | # This file must be used using `source bin/activate.fish` *within a running fish ( http://fishshell.com ) session*. 2 | # Do not run it directly. 3 | 4 | function _bashify_path -d "Converts a fish path to something bash can recognize" 5 | set fishy_path $argv 6 | set bashy_path $fishy_path[1] 7 | for path_part in $fishy_path[2..-1] 8 | set bashy_path "$bashy_path:$path_part" 9 | end 10 | echo $bashy_path 11 | end 12 | 13 | function _fishify_path -d "Converts a bash path to something fish can recognize" 14 | echo $argv | tr ':' '\n' 15 | end 16 | 17 | function deactivate -d 'Exit virtualenv mode and return to the normal environment.' 18 | # reset old environment variables 19 | if test -n "$_OLD_VIRTUAL_PATH" 20 | # https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling 21 | if test (echo $FISH_VERSION | head -c 1) -lt 3 22 | set -gx PATH (_fishify_path "$_OLD_VIRTUAL_PATH") 23 | else 24 | set -gx PATH $_OLD_VIRTUAL_PATH 25 | end 26 | set -e _OLD_VIRTUAL_PATH 27 | end 28 | 29 | if test -n "$_OLD_VIRTUAL_PYTHONHOME" 30 | set -gx PYTHONHOME "$_OLD_VIRTUAL_PYTHONHOME" 31 | set -e _OLD_VIRTUAL_PYTHONHOME 32 | end 33 | 34 | if test -n "$_OLD_FISH_PROMPT_OVERRIDE" 35 | and functions -q _old_fish_prompt 36 | # Set an empty local `$fish_function_path` to allow the removal of `fish_prompt` using `functions -e`. 37 | set -l fish_function_path 38 | 39 | # Erase virtualenv's `fish_prompt` and restore the original. 40 | functions -e fish_prompt 41 | functions -c _old_fish_prompt fish_prompt 42 | functions -e _old_fish_prompt 43 | set -e _OLD_FISH_PROMPT_OVERRIDE 44 | end 45 | 46 | set -e VIRTUAL_ENV 47 | set -e VIRTUAL_ENV_PROMPT 48 | 49 | if test "$argv[1]" != 'nondestructive' 50 | # Self-destruct! 51 | functions -e pydoc 52 | functions -e deactivate 53 | functions -e _bashify_path 54 | functions -e _fishify_path 55 | end 56 | end 57 | 58 | # Unset irrelevant variables. 59 | deactivate nondestructive 60 | 61 | set -gx VIRTUAL_ENV __VIRTUAL_ENV__ 62 | 63 | # https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling 64 | if test (echo $FISH_VERSION | head -c 1) -lt 3 65 | set -gx _OLD_VIRTUAL_PATH (_bashify_path $PATH) 66 | else 67 | set -gx _OLD_VIRTUAL_PATH $PATH 68 | end 69 | set -gx PATH "$VIRTUAL_ENV"'/'__BIN_NAME__ $PATH 70 | 71 | # Prompt override provided? 72 | # If not, just use the environment name. 73 | if test -n __VIRTUAL_PROMPT__ 74 | set -gx VIRTUAL_ENV_PROMPT __VIRTUAL_PROMPT__ 75 | else 76 | set -gx VIRTUAL_ENV_PROMPT (basename "$VIRTUAL_ENV") 77 | end 78 | 79 | # Unset `$PYTHONHOME` if set. 80 | if set -q PYTHONHOME 81 | set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME 82 | set -e PYTHONHOME 83 | end 84 | 85 | function pydoc 86 | python -m pydoc $argv 87 | end 88 | 89 | if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" 90 | # Copy the current `fish_prompt` function as `_old_fish_prompt`. 91 | functions -c fish_prompt _old_fish_prompt 92 | 93 | function fish_prompt 94 | # Run the user's prompt first; it might depend on (pipe)status. 95 | set -l prompt (_old_fish_prompt) 96 | 97 | printf '(%s) ' $VIRTUAL_ENV_PROMPT 98 | 99 | string join -- \n $prompt # handle multi-line prompts 100 | end 101 | 102 | set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" 103 | end 104 | -------------------------------------------------------------------------------- /src/virtualenv/activation/nushell/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from virtualenv.activation.via_template import ViaTemplateActivator 4 | 5 | 6 | class NushellActivator(ViaTemplateActivator): 7 | def templates(self): 8 | yield "activate.nu" 9 | 10 | @staticmethod 11 | def quote(string): 12 | """ 13 | Nushell supports raw strings like: r###'this is a string'###. 14 | 15 | This method finds the maximum continuous sharps in the string and then 16 | quote it with an extra sharp. 17 | """ 18 | max_sharps = 0 19 | current_sharps = 0 20 | for char in string: 21 | if char == "#": 22 | current_sharps += 1 23 | max_sharps = max(current_sharps, max_sharps) 24 | else: 25 | current_sharps = 0 26 | wrapping = "#" * (max_sharps + 1) 27 | return f"r{wrapping}'{string}'{wrapping}" 28 | 29 | def replacements(self, creator, dest_folder): # noqa: ARG002 30 | return { 31 | "__VIRTUAL_PROMPT__": "" if self.flag_prompt is None else self.flag_prompt, 32 | "__VIRTUAL_ENV__": str(creator.dest), 33 | "__VIRTUAL_NAME__": creator.env_name, 34 | "__BIN_NAME__": str(creator.bin_dir.relative_to(creator.dest)), 35 | } 36 | 37 | 38 | __all__ = [ 39 | "NushellActivator", 40 | ] 41 | -------------------------------------------------------------------------------- /src/virtualenv/activation/nushell/activate.nu: -------------------------------------------------------------------------------- 1 | # virtualenv activation module 2 | # Activate with `overlay use activate.nu` 3 | # Deactivate with `deactivate`, as usual 4 | # 5 | # To customize the overlay name, you can call `overlay use activate.nu as foo`, 6 | # but then simply `deactivate` won't work because it is just an alias to hide 7 | # the "activate" overlay. You'd need to call `overlay hide foo` manually. 8 | 9 | export-env { 10 | def is-string [x] { 11 | ($x | describe) == 'string' 12 | } 13 | 14 | def has-env [...names] { 15 | $names | each {|n| 16 | $n in $env 17 | } | all {|i| $i == true} 18 | } 19 | 20 | # Emulates a `test -z`, but better as it handles e.g 'false' 21 | def is-env-true [name: string] { 22 | if (has-env $name) { 23 | # Try to parse 'true', '0', '1', and fail if not convertible 24 | let parsed = (do -i { $env | get $name | into bool }) 25 | if ($parsed | describe) == 'bool' { 26 | $parsed 27 | } else { 28 | not ($env | get -i $name | is-empty) 29 | } 30 | } else { 31 | false 32 | } 33 | } 34 | 35 | let virtual_env = __VIRTUAL_ENV__ 36 | let bin = __BIN_NAME__ 37 | 38 | let is_windows = ($nu.os-info.family) == 'windows' 39 | let path_name = (if (has-env 'Path') { 40 | 'Path' 41 | } else { 42 | 'PATH' 43 | } 44 | ) 45 | 46 | let venv_path = ([$virtual_env $bin] | path join) 47 | let new_path = ($env | get $path_name | prepend $venv_path) 48 | 49 | # If there is no default prompt, then use the env name instead 50 | let virtual_env_prompt = (if (__VIRTUAL_PROMPT__ | is-empty) { 51 | ($virtual_env | path basename) 52 | } else { 53 | __VIRTUAL_PROMPT__ 54 | }) 55 | 56 | let new_env = { 57 | $path_name : $new_path 58 | VIRTUAL_ENV : $virtual_env 59 | VIRTUAL_ENV_PROMPT : $virtual_env_prompt 60 | } 61 | 62 | let new_env = (if (is-env-true 'VIRTUAL_ENV_DISABLE_PROMPT') { 63 | $new_env 64 | } else { 65 | # Creating the new prompt for the session 66 | let virtual_prefix = $'(char lparen)($virtual_env_prompt)(char rparen) ' 67 | 68 | # Back up the old prompt builder 69 | let old_prompt_command = (if (has-env 'PROMPT_COMMAND') { 70 | $env.PROMPT_COMMAND 71 | } else { 72 | '' 73 | }) 74 | 75 | let new_prompt = (if (has-env 'PROMPT_COMMAND') { 76 | if 'closure' in ($old_prompt_command | describe) { 77 | {|| $'($virtual_prefix)(do $old_prompt_command)' } 78 | } else { 79 | {|| $'($virtual_prefix)($old_prompt_command)' } 80 | } 81 | } else { 82 | {|| $'($virtual_prefix)' } 83 | }) 84 | 85 | $new_env | merge { 86 | PROMPT_COMMAND : $new_prompt 87 | VIRTUAL_PREFIX : $virtual_prefix 88 | } 89 | }) 90 | 91 | # Environment variables that will be loaded as the virtual env 92 | load-env $new_env 93 | } 94 | 95 | export alias pydoc = python -m pydoc 96 | export alias deactivate = overlay hide activate 97 | -------------------------------------------------------------------------------- /src/virtualenv/activation/powershell/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from virtualenv.activation.via_template import ViaTemplateActivator 4 | 5 | 6 | class PowerShellActivator(ViaTemplateActivator): 7 | def templates(self): 8 | yield "activate.ps1" 9 | 10 | @staticmethod 11 | def quote(string): 12 | """ 13 | This should satisfy PowerShell quoting rules [1], unless the quoted 14 | string is passed directly to Windows native commands [2]. 15 | 16 | [1]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules 17 | [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing#passing-arguments-that-contain-quote-characters 18 | """ # noqa: D205 19 | string = string.replace("'", "''") 20 | return f"'{string}'" 21 | 22 | 23 | __all__ = [ 24 | "PowerShellActivator", 25 | ] 26 | -------------------------------------------------------------------------------- /src/virtualenv/activation/powershell/activate.ps1: -------------------------------------------------------------------------------- 1 | $script:THIS_PATH = $myinvocation.mycommand.path 2 | $script:BASE_DIR = Split-Path (Resolve-Path "$THIS_PATH/..") -Parent 3 | 4 | function global:deactivate([switch] $NonDestructive) { 5 | if (Test-Path variable:_OLD_VIRTUAL_PATH) { 6 | $env:PATH = $variable:_OLD_VIRTUAL_PATH 7 | Remove-Variable "_OLD_VIRTUAL_PATH" -Scope global 8 | } 9 | 10 | if (Test-Path function:_old_virtual_prompt) { 11 | $function:prompt = $function:_old_virtual_prompt 12 | Remove-Item function:\_old_virtual_prompt 13 | } 14 | 15 | if ($env:VIRTUAL_ENV) { 16 | Remove-Item env:VIRTUAL_ENV -ErrorAction SilentlyContinue 17 | } 18 | 19 | if ($env:VIRTUAL_ENV_PROMPT) { 20 | Remove-Item env:VIRTUAL_ENV_PROMPT -ErrorAction SilentlyContinue 21 | } 22 | 23 | if (!$NonDestructive) { 24 | # Self destruct! 25 | Remove-Item function:deactivate 26 | Remove-Item function:pydoc 27 | } 28 | } 29 | 30 | function global:pydoc { 31 | python -m pydoc $args 32 | } 33 | 34 | # unset irrelevant variables 35 | deactivate -nondestructive 36 | 37 | $VIRTUAL_ENV = $BASE_DIR 38 | $env:VIRTUAL_ENV = $VIRTUAL_ENV 39 | 40 | if (__VIRTUAL_PROMPT__ -ne "") { 41 | $env:VIRTUAL_ENV_PROMPT = __VIRTUAL_PROMPT__ 42 | } 43 | else { 44 | $env:VIRTUAL_ENV_PROMPT = $( Split-Path $env:VIRTUAL_ENV -Leaf ) 45 | } 46 | 47 | New-Variable -Scope global -Name _OLD_VIRTUAL_PATH -Value $env:PATH 48 | 49 | $env:PATH = "$env:VIRTUAL_ENV/" + __BIN_NAME__ + __PATH_SEP__ + $env:PATH 50 | if (!$env:VIRTUAL_ENV_DISABLE_PROMPT) { 51 | function global:_old_virtual_prompt { 52 | "" 53 | } 54 | $function:_old_virtual_prompt = $function:prompt 55 | 56 | function global:prompt { 57 | # Add the custom prefix to the existing prompt 58 | $previous_prompt_value = & $function:_old_virtual_prompt 59 | ("(" + $env:VIRTUAL_ENV_PROMPT + ") " + $previous_prompt_value) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/virtualenv/activation/python/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from collections import OrderedDict 5 | 6 | from virtualenv.activation.via_template import ViaTemplateActivator 7 | 8 | 9 | class PythonActivator(ViaTemplateActivator): 10 | def templates(self): 11 | yield "activate_this.py" 12 | 13 | @staticmethod 14 | def quote(string): 15 | return repr(string) 16 | 17 | def replacements(self, creator, dest_folder): 18 | replacements = super().replacements(creator, dest_folder) 19 | lib_folders = OrderedDict((os.path.relpath(str(i), str(dest_folder)), None) for i in creator.libs) 20 | lib_folders = os.pathsep.join(lib_folders.keys()) 21 | replacements.update( 22 | { 23 | "__LIB_FOLDERS__": lib_folders, 24 | "__DECODE_PATH__": "", 25 | }, 26 | ) 27 | return replacements 28 | 29 | 30 | __all__ = [ 31 | "PythonActivator", 32 | ] 33 | -------------------------------------------------------------------------------- /src/virtualenv/activation/python/activate_this.py: -------------------------------------------------------------------------------- 1 | """ 2 | Activate virtualenv for current interpreter: 3 | 4 | import runpy 5 | runpy.run_path(this_file) 6 | 7 | This can be used when you must use an existing Python interpreter, not the virtualenv bin/python. 8 | """ # noqa: D415 9 | 10 | from __future__ import annotations 11 | 12 | import os 13 | import site 14 | import sys 15 | 16 | try: 17 | abs_file = os.path.abspath(__file__) 18 | except NameError as exc: 19 | msg = "You must use import runpy; runpy.run_path(this_file)" 20 | raise AssertionError(msg) from exc 21 | 22 | bin_dir = os.path.dirname(abs_file) 23 | base = bin_dir[: -len(__BIN_NAME__) - 1] # strip away the bin part from the __file__, plus the path separator 24 | 25 | # prepend bin to PATH (this file is inside the bin directory) 26 | os.environ["PATH"] = os.pathsep.join([bin_dir, *os.environ.get("PATH", "").split(os.pathsep)]) 27 | os.environ["VIRTUAL_ENV"] = base # virtual env is right above bin directory 28 | os.environ["VIRTUAL_ENV_PROMPT"] = __VIRTUAL_PROMPT__ or os.path.basename(base) 29 | 30 | # add the virtual environments libraries to the host python import mechanism 31 | prev_length = len(sys.path) 32 | for lib in __LIB_FOLDERS__.split(os.pathsep): 33 | path = os.path.realpath(os.path.join(bin_dir, lib)) 34 | site.addsitedir(path.decode("utf-8") if __DECODE_PATH__ else path) 35 | sys.path[:] = sys.path[prev_length:] + sys.path[0:prev_length] 36 | 37 | sys.real_prefix = sys.prefix 38 | sys.prefix = base 39 | -------------------------------------------------------------------------------- /src/virtualenv/activation/via_template.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import shlex 5 | import sys 6 | from abc import ABC, abstractmethod 7 | 8 | from .activator import Activator 9 | 10 | if sys.version_info >= (3, 10): 11 | from importlib.resources import files 12 | 13 | def read_binary(module_name: str, filename: str) -> bytes: 14 | return (files(module_name) / filename).read_bytes() 15 | 16 | else: 17 | from importlib.resources import read_binary 18 | 19 | 20 | class ViaTemplateActivator(Activator, ABC): 21 | @abstractmethod 22 | def templates(self): 23 | raise NotImplementedError 24 | 25 | @staticmethod 26 | def quote(string): 27 | """ 28 | Quote strings in the activation script. 29 | 30 | :param string: the string to quote 31 | :return: quoted string that works in the activation script 32 | """ 33 | return shlex.quote(string) 34 | 35 | def generate(self, creator): 36 | dest_folder = creator.bin_dir 37 | replacements = self.replacements(creator, dest_folder) 38 | generated = self._generate(replacements, self.templates(), dest_folder, creator) 39 | if self.flag_prompt is not None: 40 | creator.pyenv_cfg["prompt"] = self.flag_prompt 41 | return generated 42 | 43 | def replacements(self, creator, dest_folder): # noqa: ARG002 44 | return { 45 | "__VIRTUAL_PROMPT__": "" if self.flag_prompt is None else self.flag_prompt, 46 | "__VIRTUAL_ENV__": str(creator.dest), 47 | "__VIRTUAL_NAME__": creator.env_name, 48 | "__BIN_NAME__": str(creator.bin_dir.relative_to(creator.dest)), 49 | "__PATH_SEP__": os.pathsep, 50 | } 51 | 52 | def _generate(self, replacements, templates, to_folder, creator): 53 | generated = [] 54 | for template in templates: 55 | text = self.instantiate_template(replacements, template, creator) 56 | dest = to_folder / self.as_name(template) 57 | # remove the file if it already exists - this prevents permission 58 | # errors when the dest is not writable 59 | if dest.exists(): 60 | dest.unlink() 61 | # Powershell assumes Windows 1252 encoding when reading files without BOM 62 | encoding = "utf-8-sig" if str(template).endswith(".ps1") else "utf-8" 63 | # use write_bytes to avoid platform specific line normalization (\n -> \r\n) 64 | dest.write_bytes(text.encode(encoding)) 65 | generated.append(dest) 66 | return generated 67 | 68 | def as_name(self, template): 69 | return template 70 | 71 | def instantiate_template(self, replacements, template, creator): 72 | # read content as binary to avoid platform specific line normalization (\n -> \r\n) 73 | binary = read_binary(self.__module__, template) 74 | text = binary.decode("utf-8", errors="strict") 75 | for key, value in replacements.items(): 76 | value_uni = self._repr_unicode(creator, value) 77 | text = text.replace(key, self.quote(value_uni)) 78 | return text 79 | 80 | @staticmethod 81 | def _repr_unicode(creator, value): # noqa: ARG004 82 | return value # by default, we just let it be unicode 83 | 84 | 85 | __all__ = [ 86 | "ViaTemplateActivator", 87 | ] 88 | -------------------------------------------------------------------------------- /src/virtualenv/app_data/__init__.py: -------------------------------------------------------------------------------- 1 | """Application data stored by virtualenv.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | import os 7 | 8 | from platformdirs import user_data_dir 9 | 10 | from .na import AppDataDisabled 11 | from .read_only import ReadOnlyAppData 12 | from .via_disk_folder import AppDataDiskFolder 13 | from .via_tempdir import TempAppData 14 | 15 | LOGGER = logging.getLogger(__name__) 16 | 17 | 18 | def _default_app_data_dir(env): 19 | key = "VIRTUALENV_OVERRIDE_APP_DATA" 20 | if key in env: 21 | return env[key] 22 | return user_data_dir(appname="virtualenv", appauthor="pypa") 23 | 24 | 25 | def make_app_data(folder, **kwargs): 26 | is_read_only = kwargs.pop("read_only") 27 | env = kwargs.pop("env") 28 | if kwargs: # py3+ kwonly 29 | msg = "unexpected keywords: {}" 30 | raise TypeError(msg) 31 | 32 | if folder is None: 33 | folder = _default_app_data_dir(env) 34 | folder = os.path.abspath(folder) 35 | 36 | if is_read_only: 37 | return ReadOnlyAppData(folder) 38 | 39 | if not os.path.isdir(folder): 40 | try: 41 | os.makedirs(folder) 42 | LOGGER.debug("created app data folder %s", folder) 43 | except OSError as exception: 44 | LOGGER.info("could not create app data folder %s due to %r", folder, exception) 45 | 46 | if os.access(folder, os.W_OK): 47 | return AppDataDiskFolder(folder) 48 | LOGGER.debug("app data folder %s has no write access", folder) 49 | return TempAppData() 50 | 51 | 52 | __all__ = ( 53 | "AppDataDisabled", 54 | "AppDataDiskFolder", 55 | "ReadOnlyAppData", 56 | "TempAppData", 57 | "make_app_data", 58 | ) 59 | -------------------------------------------------------------------------------- /src/virtualenv/app_data/base.py: -------------------------------------------------------------------------------- 1 | """Application data stored by virtualenv.""" 2 | 3 | from __future__ import annotations 4 | 5 | from abc import ABC, abstractmethod 6 | from contextlib import contextmanager 7 | 8 | from virtualenv.info import IS_ZIPAPP 9 | 10 | 11 | class AppData(ABC): 12 | """Abstract storage interface for the virtualenv application.""" 13 | 14 | @abstractmethod 15 | def close(self): 16 | """Called before virtualenv exits.""" 17 | 18 | @abstractmethod 19 | def reset(self): 20 | """Called when the user passes in the reset app data.""" 21 | 22 | @abstractmethod 23 | def py_info(self, path): 24 | raise NotImplementedError 25 | 26 | @abstractmethod 27 | def py_info_clear(self): 28 | raise NotImplementedError 29 | 30 | @property 31 | def can_update(self): 32 | raise NotImplementedError 33 | 34 | @abstractmethod 35 | def embed_update_log(self, distribution, for_py_version): 36 | raise NotImplementedError 37 | 38 | @property 39 | def house(self): 40 | raise NotImplementedError 41 | 42 | @property 43 | def transient(self): 44 | raise NotImplementedError 45 | 46 | @abstractmethod 47 | def wheel_image(self, for_py_version, name): 48 | raise NotImplementedError 49 | 50 | @contextmanager 51 | def ensure_extracted(self, path, to_folder=None): 52 | """Some paths might be within the zipapp, unzip these to a path on the disk.""" 53 | if IS_ZIPAPP: 54 | with self.extract(path, to_folder) as result: 55 | yield result 56 | else: 57 | yield path 58 | 59 | @abstractmethod 60 | @contextmanager 61 | def extract(self, path, to_folder): 62 | raise NotImplementedError 63 | 64 | @abstractmethod 65 | @contextmanager 66 | def locked(self, path): 67 | raise NotImplementedError 68 | 69 | 70 | class ContentStore(ABC): 71 | @abstractmethod 72 | def exists(self): 73 | raise NotImplementedError 74 | 75 | @abstractmethod 76 | def read(self): 77 | raise NotImplementedError 78 | 79 | @abstractmethod 80 | def write(self, content): 81 | raise NotImplementedError 82 | 83 | @abstractmethod 84 | def remove(self): 85 | raise NotImplementedError 86 | 87 | @abstractmethod 88 | @contextmanager 89 | def locked(self): 90 | pass 91 | 92 | 93 | __all__ = [ 94 | "AppData", 95 | "ContentStore", 96 | ] 97 | -------------------------------------------------------------------------------- /src/virtualenv/app_data/na.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from contextlib import contextmanager 4 | 5 | from .base import AppData, ContentStore 6 | 7 | 8 | class AppDataDisabled(AppData): 9 | """No application cache available (most likely as we don't have write permissions).""" 10 | 11 | transient = True 12 | can_update = False 13 | 14 | def __init__(self) -> None: 15 | pass 16 | 17 | error = RuntimeError("no app data folder available, probably no write access to the folder") 18 | 19 | def close(self): 20 | """Do nothing.""" 21 | 22 | def reset(self): 23 | """Do nothing.""" 24 | 25 | def py_info(self, path): # noqa: ARG002 26 | return ContentStoreNA() 27 | 28 | def embed_update_log(self, distribution, for_py_version): # noqa: ARG002 29 | return ContentStoreNA() 30 | 31 | def extract(self, path, to_folder): # noqa: ARG002 32 | raise self.error 33 | 34 | @contextmanager 35 | def locked(self, path): # noqa: ARG002 36 | """Do nothing.""" 37 | yield 38 | 39 | @property 40 | def house(self): 41 | raise self.error 42 | 43 | def wheel_image(self, for_py_version, name): # noqa: ARG002 44 | raise self.error 45 | 46 | def py_info_clear(self): 47 | """Nothing to clear.""" 48 | 49 | 50 | class ContentStoreNA(ContentStore): 51 | def exists(self): 52 | return False 53 | 54 | def read(self): 55 | """Nothing to read.""" 56 | return 57 | 58 | def write(self, content): 59 | """Nothing to write.""" 60 | 61 | def remove(self): 62 | """Nothing to remove.""" 63 | 64 | @contextmanager 65 | def locked(self): 66 | yield 67 | 68 | 69 | __all__ = [ 70 | "AppDataDisabled", 71 | "ContentStoreNA", 72 | ] 73 | -------------------------------------------------------------------------------- /src/virtualenv/app_data/read_only.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os.path 4 | 5 | from virtualenv.util.lock import NoOpFileLock 6 | 7 | from .via_disk_folder import AppDataDiskFolder, PyInfoStoreDisk 8 | 9 | 10 | class ReadOnlyAppData(AppDataDiskFolder): 11 | can_update = False 12 | 13 | def __init__(self, folder: str) -> None: 14 | if not os.path.isdir(folder): 15 | msg = f"read-only app data directory {folder} does not exist" 16 | raise RuntimeError(msg) 17 | super().__init__(folder) 18 | self.lock = NoOpFileLock(folder) 19 | 20 | def reset(self) -> None: 21 | msg = "read-only app data does not support reset" 22 | raise RuntimeError(msg) 23 | 24 | def py_info_clear(self) -> None: 25 | raise NotImplementedError 26 | 27 | def py_info(self, path): 28 | return _PyInfoStoreDiskReadOnly(self.py_info_at, path) 29 | 30 | def embed_update_log(self, distribution, for_py_version): 31 | raise NotImplementedError 32 | 33 | 34 | class _PyInfoStoreDiskReadOnly(PyInfoStoreDisk): 35 | def write(self, content): # noqa: ARG002 36 | msg = "read-only app data python info cannot be updated" 37 | raise RuntimeError(msg) 38 | 39 | 40 | __all__ = [ 41 | "ReadOnlyAppData", 42 | ] 43 | -------------------------------------------------------------------------------- /src/virtualenv/app_data/via_tempdir.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from tempfile import mkdtemp 5 | 6 | from virtualenv.util.path import safe_delete 7 | 8 | from .via_disk_folder import AppDataDiskFolder 9 | 10 | LOGGER = logging.getLogger(__name__) 11 | 12 | 13 | class TempAppData(AppDataDiskFolder): 14 | transient = True 15 | can_update = False 16 | 17 | def __init__(self) -> None: 18 | super().__init__(folder=mkdtemp()) 19 | LOGGER.debug("created temporary app data folder %s", self.lock.path) 20 | 21 | def reset(self): 22 | """This is a temporary folder, is already empty to start with.""" 23 | 24 | def close(self): 25 | LOGGER.debug("remove temporary app data folder %s", self.lock.path) 26 | safe_delete(self.lock.path) 27 | 28 | def embed_update_log(self, distribution, for_py_version): 29 | raise NotImplementedError 30 | 31 | 32 | __all__ = [ 33 | "TempAppData", 34 | ] 35 | -------------------------------------------------------------------------------- /src/virtualenv/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/config/__init__.py -------------------------------------------------------------------------------- /src/virtualenv/config/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/config/cli/__init__.py -------------------------------------------------------------------------------- /src/virtualenv/config/convert.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os 5 | from typing import ClassVar 6 | 7 | LOGGER = logging.getLogger(__name__) 8 | 9 | 10 | class TypeData: 11 | def __init__(self, default_type, as_type) -> None: 12 | self.default_type = default_type 13 | self.as_type = as_type 14 | 15 | def __repr__(self) -> str: 16 | return f"{self.__class__.__name__}(base={self.default_type}, as={self.as_type})" 17 | 18 | def convert(self, value): 19 | return self.default_type(value) 20 | 21 | 22 | class BoolType(TypeData): 23 | BOOLEAN_STATES: ClassVar[dict[str, bool]] = { 24 | "1": True, 25 | "yes": True, 26 | "true": True, 27 | "on": True, 28 | "0": False, 29 | "no": False, 30 | "false": False, 31 | "off": False, 32 | } 33 | 34 | def convert(self, value): 35 | if value.lower() not in self.BOOLEAN_STATES: 36 | msg = f"Not a boolean: {value}" 37 | raise ValueError(msg) 38 | return self.BOOLEAN_STATES[value.lower()] 39 | 40 | 41 | class NoneType(TypeData): 42 | def convert(self, value): 43 | if not value: 44 | return None 45 | return str(value) 46 | 47 | 48 | class ListType(TypeData): 49 | def _validate(self): 50 | """no op.""" 51 | 52 | def convert(self, value, flatten=True): # noqa: ARG002, FBT002 53 | values = self.split_values(value) 54 | result = [] 55 | for a_value in values: 56 | sub_values = a_value.split(os.pathsep) 57 | result.extend(sub_values) 58 | return [self.as_type(i) for i in result] 59 | 60 | def split_values(self, value): 61 | """ 62 | Split the provided value into a list. 63 | 64 | First this is done by newlines. If there were no newlines in the text, 65 | then we next try to split by comma. 66 | """ 67 | if isinstance(value, (str, bytes)): 68 | # Use `splitlines` rather than a custom check for whether there is 69 | # more than one line. This ensures that the full `splitlines()` 70 | # logic is supported here. 71 | values = value.splitlines() 72 | if len(values) <= 1: 73 | values = value.split(",") 74 | values = filter(None, [x.strip() for x in values]) 75 | else: 76 | values = list(value) 77 | 78 | return values 79 | 80 | 81 | def convert(value, as_type, source): 82 | """Convert the value as a given type where the value comes from the given source.""" 83 | try: 84 | return as_type.convert(value) 85 | except Exception as exception: 86 | LOGGER.warning("%s failed to convert %r as %r because %r", source, value, as_type, exception) 87 | raise 88 | 89 | 90 | _CONVERT = {bool: BoolType, type(None): NoneType, list: ListType} 91 | 92 | 93 | def get_type(action): 94 | default_type = type(action.default) 95 | as_type = default_type if action.type is None else action.type 96 | return _CONVERT.get(default_type, TypeData)(default_type, as_type) 97 | 98 | 99 | __all__ = [ 100 | "convert", 101 | "get_type", 102 | ] 103 | -------------------------------------------------------------------------------- /src/virtualenv/config/env_var.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from contextlib import suppress 4 | 5 | from .convert import convert 6 | 7 | 8 | def get_env_var(key, as_type, env): 9 | """ 10 | Get the environment variable option. 11 | 12 | :param key: the config key requested 13 | :param as_type: the type we would like to convert it to 14 | :param env: environment variables to use 15 | :return: 16 | """ 17 | environ_key = f"VIRTUALENV_{key.upper()}" 18 | if env.get(environ_key): 19 | value = env[environ_key] 20 | 21 | with suppress(Exception): # note the converter already logs a warning when failures happen 22 | source = f"env var {environ_key}" 23 | as_type = convert(value, as_type, source) 24 | return as_type, source 25 | return None 26 | 27 | 28 | __all__ = [ 29 | "get_env_var", 30 | ] 31 | -------------------------------------------------------------------------------- /src/virtualenv/config/ini.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os 5 | from configparser import ConfigParser 6 | from pathlib import Path 7 | from typing import ClassVar 8 | 9 | from platformdirs import user_config_dir 10 | 11 | from .convert import convert 12 | 13 | LOGGER = logging.getLogger(__name__) 14 | 15 | 16 | class IniConfig: 17 | VIRTUALENV_CONFIG_FILE_ENV_VAR: ClassVar[str] = "VIRTUALENV_CONFIG_FILE" 18 | STATE: ClassVar[dict[bool | None, str]] = {None: "failed to parse", True: "active", False: "missing"} 19 | 20 | section = "virtualenv" 21 | 22 | def __init__(self, env=None) -> None: 23 | env = os.environ if env is None else env 24 | config_file = env.get(self.VIRTUALENV_CONFIG_FILE_ENV_VAR, None) 25 | self.is_env_var = config_file is not None 26 | if config_file is None: 27 | config_file = Path(user_config_dir(appname="virtualenv", appauthor="pypa")) / "virtualenv.ini" 28 | else: 29 | config_file = Path(config_file) 30 | self.config_file = config_file 31 | self._cache = {} 32 | 33 | exception = None 34 | self.has_config_file = None 35 | try: 36 | self.has_config_file = self.config_file.exists() 37 | except OSError as exc: 38 | exception = exc 39 | else: 40 | if self.has_config_file: 41 | self.config_file = self.config_file.resolve() 42 | self.config_parser = ConfigParser() 43 | try: 44 | self._load() 45 | self.has_virtualenv_section = self.config_parser.has_section(self.section) 46 | except Exception as exc: # noqa: BLE001 47 | exception = exc 48 | if exception is not None: 49 | LOGGER.error("failed to read config file %s because %r", config_file, exception) 50 | 51 | def _load(self): 52 | with self.config_file.open("rt", encoding="utf-8") as file_handler: 53 | return self.config_parser.read_file(file_handler) 54 | 55 | def get(self, key, as_type): 56 | cache_key = key, as_type 57 | if cache_key in self._cache: 58 | return self._cache[cache_key] 59 | try: 60 | source = "file" 61 | raw_value = self.config_parser.get(self.section, key.lower()) 62 | value = convert(raw_value, as_type, source) 63 | result = value, source 64 | except Exception: # noqa: BLE001 65 | result = None 66 | self._cache[cache_key] = result 67 | return result 68 | 69 | def __bool__(self) -> bool: 70 | return bool(self.has_config_file) and bool(self.has_virtualenv_section) 71 | 72 | @property 73 | def epilog(self): 74 | return ( 75 | f"\nconfig file {self.config_file} {self.STATE[self.has_config_file]} " 76 | f"(change{'d' if self.is_env_var else ''} via env var {self.VIRTUALENV_CONFIG_FILE_ENV_VAR})" 77 | ) 78 | -------------------------------------------------------------------------------- /src/virtualenv/create/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/create/__init__.py -------------------------------------------------------------------------------- /src/virtualenv/create/debug.py: -------------------------------------------------------------------------------- 1 | """Inspect a target Python interpreter virtual environment wise.""" 2 | 3 | from __future__ import annotations 4 | 5 | import sys # built-in 6 | 7 | 8 | def encode_path(value): 9 | if value is None: 10 | return None 11 | if not isinstance(value, (str, bytes)): 12 | value = repr(value) if isinstance(value, type) else repr(type(value)) 13 | if isinstance(value, bytes): 14 | value = value.decode(sys.getfilesystemencoding()) 15 | return value 16 | 17 | 18 | def encode_list_path(value): 19 | return [encode_path(i) for i in value] 20 | 21 | 22 | def run(): 23 | """Print debug data about the virtual environment.""" 24 | try: 25 | from collections import OrderedDict # noqa: PLC0415 26 | except ImportError: # pragma: no cover 27 | # this is possible if the standard library cannot be accessed 28 | 29 | OrderedDict = dict # pragma: no cover # noqa: N806 30 | result = OrderedDict([("sys", OrderedDict())]) 31 | path_keys = ( 32 | "executable", 33 | "_base_executable", 34 | "prefix", 35 | "base_prefix", 36 | "real_prefix", 37 | "exec_prefix", 38 | "base_exec_prefix", 39 | "path", 40 | "meta_path", 41 | ) 42 | for key in path_keys: 43 | value = getattr(sys, key, None) 44 | value = encode_list_path(value) if isinstance(value, list) else encode_path(value) 45 | result["sys"][key] = value 46 | result["sys"]["fs_encoding"] = sys.getfilesystemencoding() 47 | result["sys"]["io_encoding"] = getattr(sys.stdout, "encoding", None) 48 | result["version"] = sys.version 49 | 50 | try: 51 | import sysconfig # noqa: PLC0415 52 | 53 | # https://bugs.python.org/issue22199 54 | makefile = getattr(sysconfig, "get_makefile_filename", getattr(sysconfig, "_get_makefile_filename", None)) 55 | result["makefile_filename"] = encode_path(makefile()) 56 | except ImportError: 57 | pass 58 | 59 | import os # landmark # noqa: PLC0415 60 | 61 | result["os"] = repr(os) 62 | 63 | try: 64 | import site # site # noqa: PLC0415 65 | 66 | result["site"] = repr(site) 67 | except ImportError as exception: # pragma: no cover 68 | result["site"] = repr(exception) # pragma: no cover 69 | 70 | try: 71 | import datetime # site # noqa: PLC0415 72 | 73 | result["datetime"] = repr(datetime) 74 | except ImportError as exception: # pragma: no cover 75 | result["datetime"] = repr(exception) # pragma: no cover 76 | 77 | try: 78 | import math # site # noqa: PLC0415 79 | 80 | result["math"] = repr(math) 81 | except ImportError as exception: # pragma: no cover 82 | result["math"] = repr(exception) # pragma: no cover 83 | 84 | # try to print out, this will validate if other core modules are available (json in this case) 85 | try: 86 | import json # noqa: PLC0415 87 | 88 | result["json"] = repr(json) 89 | except ImportError as exception: 90 | result["json"] = repr(exception) 91 | else: 92 | try: 93 | content = json.dumps(result, indent=2) 94 | sys.stdout.write(content) 95 | except (ValueError, TypeError) as exception: # pragma: no cover 96 | sys.stderr.write(repr(exception)) 97 | sys.stdout.write(repr(result)) # pragma: no cover 98 | raise SystemExit(1) # noqa: B904 # pragma: no cover 99 | 100 | 101 | if __name__ == "__main__": 102 | run() 103 | -------------------------------------------------------------------------------- /src/virtualenv/create/describe.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | from collections import OrderedDict 5 | from pathlib import Path 6 | 7 | from virtualenv.info import IS_WIN 8 | 9 | 10 | class Describe: 11 | """Given a host interpreter tell us information about what the created interpreter might look like.""" 12 | 13 | suffix = ".exe" if IS_WIN else "" 14 | 15 | def __init__(self, dest, interpreter) -> None: 16 | self.interpreter = interpreter 17 | self.dest = dest 18 | self._stdlib = None 19 | self._stdlib_platform = None 20 | self._system_stdlib = None 21 | self._conf_vars = None 22 | 23 | @property 24 | def bin_dir(self): 25 | return self.script_dir 26 | 27 | @property 28 | def script_dir(self): 29 | return self.dest / self.interpreter.install_path("scripts") 30 | 31 | @property 32 | def purelib(self): 33 | return self.dest / self.interpreter.install_path("purelib") 34 | 35 | @property 36 | def platlib(self): 37 | return self.dest / self.interpreter.install_path("platlib") 38 | 39 | @property 40 | def libs(self): 41 | return list(OrderedDict(((self.platlib, None), (self.purelib, None))).keys()) 42 | 43 | @property 44 | def stdlib(self): 45 | if self._stdlib is None: 46 | self._stdlib = Path(self.interpreter.sysconfig_path("stdlib", config_var=self._config_vars)) 47 | return self._stdlib 48 | 49 | @property 50 | def stdlib_platform(self): 51 | if self._stdlib_platform is None: 52 | self._stdlib_platform = Path(self.interpreter.sysconfig_path("platstdlib", config_var=self._config_vars)) 53 | return self._stdlib_platform 54 | 55 | @property 56 | def _config_vars(self): 57 | if self._conf_vars is None: 58 | self._conf_vars = self._calc_config_vars(self.dest) 59 | return self._conf_vars 60 | 61 | def _calc_config_vars(self, to): 62 | sys_vars = self.interpreter.sysconfig_vars 63 | return {k: (to if v is not None and v.startswith(self.interpreter.prefix) else v) for k, v in sys_vars.items()} 64 | 65 | @classmethod 66 | def can_describe(cls, interpreter): # noqa: ARG003 67 | """Knows means it knows how the output will look.""" 68 | return True 69 | 70 | @property 71 | def env_name(self): 72 | return self.dest.parts[-1] 73 | 74 | @property 75 | def exe(self): 76 | return self.bin_dir / f"{self.exe_stem()}{self.suffix}" 77 | 78 | @classmethod 79 | def exe_stem(cls): 80 | """Executable name without suffix - there seems to be no standard way to get this without creating it.""" 81 | raise NotImplementedError 82 | 83 | def script(self, name): 84 | return self.script_dir / f"{name}{self.suffix}" 85 | 86 | 87 | class Python3Supports(Describe, ABC): 88 | @classmethod 89 | def can_describe(cls, interpreter): 90 | return interpreter.version_info.major == 3 and super().can_describe(interpreter) # noqa: PLR2004 91 | 92 | 93 | class PosixSupports(Describe, ABC): 94 | @classmethod 95 | def can_describe(cls, interpreter): 96 | return interpreter.os == "posix" and super().can_describe(interpreter) 97 | 98 | 99 | class WindowsSupports(Describe, ABC): 100 | @classmethod 101 | def can_describe(cls, interpreter): 102 | return interpreter.os == "nt" and super().can_describe(interpreter) 103 | 104 | 105 | __all__ = [ 106 | "Describe", 107 | "PosixSupports", 108 | "Python3Supports", 109 | "WindowsSupports", 110 | ] 111 | -------------------------------------------------------------------------------- /src/virtualenv/create/pyenv_cfg.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os 5 | from collections import OrderedDict 6 | 7 | LOGGER = logging.getLogger(__name__) 8 | 9 | 10 | class PyEnvCfg: 11 | def __init__(self, content, path) -> None: 12 | self.content = content 13 | self.path = path 14 | 15 | @classmethod 16 | def from_folder(cls, folder): 17 | return cls.from_file(folder / "pyvenv.cfg") 18 | 19 | @classmethod 20 | def from_file(cls, path): 21 | content = cls._read_values(path) if path.exists() else OrderedDict() 22 | return PyEnvCfg(content, path) 23 | 24 | @staticmethod 25 | def _read_values(path): 26 | content = OrderedDict() 27 | for line in path.read_text(encoding="utf-8").splitlines(): 28 | equals_at = line.index("=") 29 | key = line[:equals_at].strip() 30 | value = line[equals_at + 1 :].strip() 31 | content[key] = value 32 | return content 33 | 34 | def write(self): 35 | LOGGER.debug("write %s", self.path) 36 | text = "" 37 | for key, value in self.content.items(): 38 | normalized_value = os.path.realpath(value) if value and os.path.exists(value) else value 39 | line = f"{key} = {normalized_value}" 40 | LOGGER.debug("\t%s", line) 41 | text += line 42 | text += "\n" 43 | self.path.write_text(text, encoding="utf-8") 44 | 45 | def refresh(self): 46 | self.content = self._read_values(self.path) 47 | return self.content 48 | 49 | def __setitem__(self, key, value) -> None: 50 | self.content[key] = value 51 | 52 | def __getitem__(self, key): 53 | return self.content[key] 54 | 55 | def __contains__(self, item) -> bool: 56 | return item in self.content 57 | 58 | def update(self, other): 59 | self.content.update(other) 60 | return self 61 | 62 | def __repr__(self) -> str: 63 | return f"{self.__class__.__name__}(path={self.path})" 64 | 65 | 66 | __all__ = [ 67 | "PyEnvCfg", 68 | ] 69 | -------------------------------------------------------------------------------- /src/virtualenv/create/via_global_ref/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/create/via_global_ref/__init__.py -------------------------------------------------------------------------------- /src/virtualenv/create/via_global_ref/builtin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/create/via_global_ref/builtin/__init__.py -------------------------------------------------------------------------------- /src/virtualenv/create/via_global_ref/builtin/builtin_way.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | 5 | from virtualenv.create.creator import Creator 6 | from virtualenv.create.describe import Describe 7 | 8 | 9 | class VirtualenvBuiltin(Creator, Describe, ABC): 10 | """A creator that does operations itself without delegation, if we can create it we can also describe it.""" 11 | 12 | def __init__(self, options, interpreter) -> None: 13 | Creator.__init__(self, options, interpreter) 14 | Describe.__init__(self, self.dest, interpreter) 15 | 16 | 17 | __all__ = [ 18 | "VirtualenvBuiltin", 19 | ] 20 | -------------------------------------------------------------------------------- /src/virtualenv/create/via_global_ref/builtin/cpython/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/create/via_global_ref/builtin/cpython/__init__.py -------------------------------------------------------------------------------- /src/virtualenv/create/via_global_ref/builtin/cpython/common.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | from abc import ABC 5 | from collections import OrderedDict 6 | from pathlib import Path 7 | 8 | from virtualenv.create.describe import PosixSupports, WindowsSupports 9 | from virtualenv.create.via_global_ref.builtin.ref import RefMust, RefWhen 10 | from virtualenv.create.via_global_ref.builtin.via_global_self_do import ViaGlobalRefVirtualenvBuiltin 11 | 12 | 13 | class CPython(ViaGlobalRefVirtualenvBuiltin, ABC): 14 | @classmethod 15 | def can_describe(cls, interpreter): 16 | return interpreter.implementation == "CPython" and super().can_describe(interpreter) 17 | 18 | @classmethod 19 | def exe_stem(cls): 20 | return "python" 21 | 22 | 23 | class CPythonPosix(CPython, PosixSupports, ABC): 24 | """Create a CPython virtual environment on POSIX platforms.""" 25 | 26 | @classmethod 27 | def _executables(cls, interpreter): 28 | host_exe = Path(interpreter.system_executable) 29 | major, minor = interpreter.version_info.major, interpreter.version_info.minor 30 | targets = OrderedDict((i, None) for i in ["python", f"python{major}", f"python{major}.{minor}", host_exe.name]) 31 | yield host_exe, list(targets.keys()), RefMust.NA, RefWhen.ANY 32 | 33 | 34 | class CPythonWindows(CPython, WindowsSupports, ABC): 35 | @classmethod 36 | def _executables(cls, interpreter): 37 | # symlink of the python executables does not work reliably, copy always instead 38 | # - https://bugs.python.org/issue42013 39 | # - venv 40 | host = cls.host_python(interpreter) 41 | for path in (host.parent / n for n in {"python.exe", host.name}): 42 | yield host, [path.name], RefMust.COPY, RefWhen.ANY 43 | # for more info on pythonw.exe see https://stackoverflow.com/a/30313091 44 | python_w = host.parent / "pythonw.exe" 45 | yield python_w, [python_w.name], RefMust.COPY, RefWhen.ANY 46 | 47 | @classmethod 48 | def host_python(cls, interpreter): 49 | return Path(interpreter.system_executable) 50 | 51 | 52 | def is_mac_os_framework(interpreter): 53 | if interpreter.platform == "darwin": 54 | return interpreter.sysconfig_vars.get("PYTHONFRAMEWORK") == "Python3" 55 | return False 56 | 57 | 58 | def is_macos_brew(interpreter): 59 | return interpreter.platform == "darwin" and _BREW.fullmatch(interpreter.system_prefix) is not None 60 | 61 | 62 | _BREW = re.compile( 63 | r"/(usr/local|opt/homebrew)/(opt/python@3\.\d{1,2}|Cellar/python@3\.\d{1,2}/3\.\d{1,2}\.\d{1,2})/Frameworks/" 64 | r"Python\.framework/Versions/3\.\d{1,2}", 65 | ) 66 | 67 | __all__ = [ 68 | "CPython", 69 | "CPythonPosix", 70 | "CPythonWindows", 71 | "is_mac_os_framework", 72 | "is_macos_brew", 73 | ] 74 | -------------------------------------------------------------------------------- /src/virtualenv/create/via_global_ref/builtin/graalpy/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | from pathlib import Path 5 | 6 | from virtualenv.create.describe import PosixSupports, WindowsSupports 7 | from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest, RefMust, RefWhen 8 | from virtualenv.create.via_global_ref.builtin.via_global_self_do import ViaGlobalRefVirtualenvBuiltin 9 | 10 | 11 | class GraalPy(ViaGlobalRefVirtualenvBuiltin, ABC): 12 | @classmethod 13 | def can_describe(cls, interpreter): 14 | return interpreter.implementation == "GraalVM" and super().can_describe(interpreter) 15 | 16 | @classmethod 17 | def exe_stem(cls): 18 | return "graalpy" 19 | 20 | @classmethod 21 | def exe_names(cls, interpreter): 22 | return { 23 | cls.exe_stem(), 24 | "python", 25 | f"python{interpreter.version_info.major}", 26 | f"python{interpreter.version_info.major}.{interpreter.version_info.minor}", 27 | } 28 | 29 | @classmethod 30 | def _executables(cls, interpreter): 31 | host = Path(interpreter.system_executable) 32 | targets = sorted(f"{name}{cls.suffix}" for name in cls.exe_names(interpreter)) 33 | yield host, targets, RefMust.NA, RefWhen.ANY 34 | 35 | @classmethod 36 | def sources(cls, interpreter): 37 | yield from super().sources(interpreter) 38 | python_dir = Path(interpreter.system_executable).resolve().parent 39 | if python_dir.name in {"bin", "Scripts"}: 40 | python_dir = python_dir.parent 41 | 42 | native_lib = cls._native_lib(python_dir / "lib", interpreter.platform) 43 | if native_lib.exists(): 44 | yield PathRefToDest(native_lib, dest=lambda self, s: self.bin_dir.parent / "lib" / s.name) 45 | 46 | for jvm_dir_name in ("jvm", "jvmlibs", "modules"): 47 | jvm_dir = python_dir / jvm_dir_name 48 | if jvm_dir.exists(): 49 | yield PathRefToDest(jvm_dir, dest=lambda self, s: self.bin_dir.parent / s.name) 50 | 51 | @classmethod 52 | def _shared_libs(cls, python_dir): 53 | raise NotImplementedError 54 | 55 | def set_pyenv_cfg(self): 56 | super().set_pyenv_cfg() 57 | # GraalPy 24.0 and older had home without the bin 58 | version = self.interpreter.version_info 59 | if version.major == 3 and version.minor <= 10: # noqa: PLR2004 60 | home = Path(self.pyenv_cfg["home"]) 61 | if home.name == "bin": 62 | self.pyenv_cfg["home"] = str(home.parent) 63 | 64 | 65 | class GraalPyPosix(GraalPy, PosixSupports): 66 | @classmethod 67 | def _native_lib(cls, lib_dir, platform): 68 | if platform == "darwin": 69 | return lib_dir / "libpythonvm.dylib" 70 | return lib_dir / "libpythonvm.so" 71 | 72 | 73 | class GraalPyWindows(GraalPy, WindowsSupports): 74 | @classmethod 75 | def _native_lib(cls, lib_dir, _platform): 76 | return lib_dir / "pythonvm.dll" 77 | 78 | def set_pyenv_cfg(self): 79 | # GraalPy needs an additional entry in pyvenv.cfg on Windows 80 | super().set_pyenv_cfg() 81 | self.pyenv_cfg["venvlauncher_command"] = self.interpreter.system_executable 82 | 83 | 84 | __all__ = [ 85 | "GraalPyPosix", 86 | "GraalPyWindows", 87 | ] 88 | -------------------------------------------------------------------------------- /src/virtualenv/create/via_global_ref/builtin/pypy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/create/via_global_ref/builtin/pypy/__init__.py -------------------------------------------------------------------------------- /src/virtualenv/create/via_global_ref/builtin/pypy/common.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import abc 4 | from pathlib import Path 5 | 6 | from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest, RefMust, RefWhen 7 | from virtualenv.create.via_global_ref.builtin.via_global_self_do import ViaGlobalRefVirtualenvBuiltin 8 | 9 | 10 | class PyPy(ViaGlobalRefVirtualenvBuiltin, abc.ABC): 11 | @classmethod 12 | def can_describe(cls, interpreter): 13 | return interpreter.implementation == "PyPy" and super().can_describe(interpreter) 14 | 15 | @classmethod 16 | def _executables(cls, interpreter): 17 | host = Path(interpreter.system_executable) 18 | targets = sorted(f"{name}{PyPy.suffix}" for name in cls.exe_names(interpreter)) 19 | yield host, targets, RefMust.NA, RefWhen.ANY 20 | 21 | @classmethod 22 | def executables(cls, interpreter): 23 | yield from super().sources(interpreter) 24 | 25 | @classmethod 26 | def exe_names(cls, interpreter): 27 | return { 28 | cls.exe_stem(), 29 | "python", 30 | f"python{interpreter.version_info.major}", 31 | f"python{interpreter.version_info.major}.{interpreter.version_info.minor}", 32 | } 33 | 34 | @classmethod 35 | def sources(cls, interpreter): 36 | yield from cls.executables(interpreter) 37 | for host in cls._add_shared_libs(interpreter): 38 | yield PathRefToDest(host, dest=lambda self, s: self.bin_dir / s.name) 39 | 40 | @classmethod 41 | def _add_shared_libs(cls, interpreter): 42 | # https://bitbucket.org/pypy/pypy/issue/1922/future-proofing-virtualenv 43 | python_dir = Path(interpreter.system_executable).resolve().parent 44 | yield from cls._shared_libs(python_dir) 45 | 46 | @classmethod 47 | def _shared_libs(cls, python_dir): 48 | raise NotImplementedError 49 | 50 | 51 | __all__ = [ 52 | "PyPy", 53 | ] 54 | -------------------------------------------------------------------------------- /src/virtualenv/create/via_global_ref/builtin/pypy/pypy3.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import abc 4 | from pathlib import Path 5 | 6 | from virtualenv.create.describe import PosixSupports, Python3Supports, WindowsSupports 7 | from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest 8 | 9 | from .common import PyPy 10 | 11 | 12 | class PyPy3(PyPy, Python3Supports, abc.ABC): 13 | @classmethod 14 | def exe_stem(cls): 15 | return "pypy3" 16 | 17 | @classmethod 18 | def exe_names(cls, interpreter): 19 | return super().exe_names(interpreter) | {"pypy"} 20 | 21 | 22 | class PyPy3Posix(PyPy3, PosixSupports): 23 | """PyPy 3 on POSIX.""" 24 | 25 | @classmethod 26 | def _shared_libs(cls, python_dir): 27 | # glob for libpypy3-c.so, libpypy3-c.dylib, libpypy3.9-c.so ... 28 | return python_dir.glob("libpypy3*.*") 29 | 30 | def to_lib(self, src): 31 | return self.dest / "lib" / src.name 32 | 33 | @classmethod 34 | def sources(cls, interpreter): 35 | yield from super().sources(interpreter) 36 | # PyPy >= 3.8 supports a standard prefix installation, where older 37 | # versions always used a portable/development style installation. 38 | # If this is a standard prefix installation, skip the below: 39 | if interpreter.system_prefix == "/usr": 40 | return 41 | # Also copy/symlink anything under prefix/lib, which, for "portable" 42 | # PyPy builds, includes the tk,tcl runtime and a number of shared 43 | # objects. In distro-specific builds or on conda this should be empty 44 | # (on PyPy3.8+ it will, like on CPython, hold the stdlib). 45 | host_lib = Path(interpreter.system_prefix) / "lib" 46 | stdlib = Path(interpreter.system_stdlib) 47 | if host_lib.exists() and host_lib.is_dir(): 48 | for path in host_lib.iterdir(): 49 | if stdlib == path: 50 | # For PyPy3.8+ the stdlib lives in lib/pypy3.8 51 | # We need to avoid creating a symlink to it since that 52 | # will defeat the purpose of a virtualenv 53 | continue 54 | yield PathRefToDest(path, dest=cls.to_lib) 55 | 56 | 57 | class Pypy3Windows(PyPy3, WindowsSupports): 58 | """PyPy 3 on Windows.""" 59 | 60 | @property 61 | def less_v37(self): 62 | return self.interpreter.version_info.minor < 7 # noqa: PLR2004 63 | 64 | @classmethod 65 | def _shared_libs(cls, python_dir): 66 | # glob for libpypy*.dll and libffi*.dll 67 | for pattern in ["libpypy*.dll", "libffi*.dll"]: 68 | srcs = python_dir.glob(pattern) 69 | yield from srcs 70 | 71 | 72 | __all__ = [ 73 | "PyPy3", 74 | "PyPy3Posix", 75 | "Pypy3Windows", 76 | ] 77 | -------------------------------------------------------------------------------- /src/virtualenv/create/via_global_ref/store.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | 6 | def handle_store_python(meta, interpreter): 7 | if is_store_python(interpreter): 8 | meta.symlink_error = "Windows Store Python does not support virtual environments via symlink" 9 | return meta 10 | 11 | 12 | def is_store_python(interpreter): 13 | parts = Path(interpreter.system_executable).parts 14 | return ( 15 | len(parts) > 4 # noqa: PLR2004 16 | and parts[-4] == "Microsoft" 17 | and parts[-3] == "WindowsApps" 18 | and parts[-2].startswith("PythonSoftwareFoundation.Python.3.") 19 | and parts[-1].startswith("python") 20 | ) 21 | 22 | 23 | __all__ = [ 24 | "handle_store_python", 25 | "is_store_python", 26 | ] 27 | -------------------------------------------------------------------------------- /src/virtualenv/discovery/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/discovery/__init__.py -------------------------------------------------------------------------------- /src/virtualenv/discovery/discover.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | 5 | 6 | class Discover(ABC): 7 | """Discover and provide the requested Python interpreter.""" 8 | 9 | @classmethod 10 | def add_parser_arguments(cls, parser): 11 | """ 12 | Add CLI arguments for this discovery mechanisms. 13 | 14 | :param parser: the CLI parser 15 | """ 16 | raise NotImplementedError 17 | 18 | def __init__(self, options) -> None: 19 | """ 20 | Create a new discovery mechanism. 21 | 22 | :param options: the parsed options as defined within :meth:`add_parser_arguments` 23 | """ 24 | self._has_run = False 25 | self._interpreter = None 26 | self._env = options.env 27 | 28 | @abstractmethod 29 | def run(self): 30 | """ 31 | Discovers an interpreter. 32 | 33 | :return: the interpreter ready to use for virtual environment creation 34 | """ 35 | raise NotImplementedError 36 | 37 | @property 38 | def interpreter(self): 39 | """:return: the interpreter as returned by :meth:`run`, cached""" 40 | if self._has_run is False: 41 | self._interpreter = self.run() 42 | self._has_run = True 43 | return self._interpreter 44 | 45 | 46 | __all__ = [ 47 | "Discover", 48 | ] 49 | -------------------------------------------------------------------------------- /src/virtualenv/discovery/windows/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from virtualenv.discovery.py_info import PythonInfo 4 | from virtualenv.discovery.py_spec import PythonSpec 5 | 6 | from .pep514 import discover_pythons 7 | 8 | # Map of well-known organizations (as per PEP 514 Company Windows Registry key part) versus Python implementation 9 | _IMPLEMENTATION_BY_ORG = { 10 | "ContinuumAnalytics": "CPython", 11 | "PythonCore": "CPython", 12 | } 13 | 14 | 15 | class Pep514PythonInfo(PythonInfo): 16 | """A Python information acquired from PEP-514.""" 17 | 18 | 19 | def propose_interpreters(spec, cache_dir, env): 20 | # see if PEP-514 entries are good 21 | 22 | # start with higher python versions in an effort to use the latest version available 23 | # and prefer PythonCore over conda pythons (as virtualenv is mostly used by non conda tools) 24 | existing = list(discover_pythons()) 25 | existing.sort( 26 | key=lambda i: (*tuple(-1 if j is None else j for j in i[1:4]), 1 if i[0] == "PythonCore" else 0), 27 | reverse=True, 28 | ) 29 | 30 | for name, major, minor, arch, threaded, exe, _ in existing: 31 | # Map well-known/most common organizations to a Python implementation, use the org name as a fallback for 32 | # backwards compatibility. 33 | implementation = _IMPLEMENTATION_BY_ORG.get(name, name) 34 | 35 | # Pre-filtering based on Windows Registry metadata, for CPython only 36 | skip_pre_filter = implementation.lower() != "cpython" 37 | registry_spec = PythonSpec(None, implementation, major, minor, None, arch, exe, free_threaded=threaded) 38 | if skip_pre_filter or registry_spec.satisfies(spec): 39 | interpreter = Pep514PythonInfo.from_exe(exe, cache_dir, env=env, raise_on_error=False) 40 | if interpreter is not None and interpreter.satisfies(spec, impl_must_match=True): 41 | yield interpreter # Final filtering/matching using interpreter metadata 42 | 43 | 44 | __all__ = [ 45 | "Pep514PythonInfo", 46 | "propose_interpreters", 47 | ] 48 | -------------------------------------------------------------------------------- /src/virtualenv/info.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os 5 | import platform 6 | import sys 7 | import tempfile 8 | 9 | IMPLEMENTATION = platform.python_implementation() 10 | IS_PYPY = IMPLEMENTATION == "PyPy" 11 | IS_GRAALPY = IMPLEMENTATION == "GraalVM" 12 | IS_CPYTHON = IMPLEMENTATION == "CPython" 13 | IS_WIN = sys.platform == "win32" 14 | IS_MAC_ARM64 = sys.platform == "darwin" and platform.machine() == "arm64" 15 | ROOT = os.path.realpath(os.path.join(os.path.abspath(__file__), os.path.pardir, os.path.pardir)) 16 | IS_ZIPAPP = os.path.isfile(ROOT) 17 | _CAN_SYMLINK = _FS_CASE_SENSITIVE = _CFG_DIR = _DATA_DIR = None 18 | LOGGER = logging.getLogger(__name__) 19 | 20 | 21 | def fs_is_case_sensitive(): 22 | global _FS_CASE_SENSITIVE # noqa: PLW0603 23 | 24 | if _FS_CASE_SENSITIVE is None: 25 | with tempfile.NamedTemporaryFile(prefix="TmP") as tmp_file: 26 | _FS_CASE_SENSITIVE = not os.path.exists(tmp_file.name.lower()) 27 | LOGGER.debug("filesystem is %scase-sensitive", "" if _FS_CASE_SENSITIVE else "not ") 28 | return _FS_CASE_SENSITIVE 29 | 30 | 31 | def fs_supports_symlink(): 32 | global _CAN_SYMLINK # noqa: PLW0603 33 | 34 | if _CAN_SYMLINK is None: 35 | can = False 36 | if hasattr(os, "symlink"): 37 | if IS_WIN: 38 | with tempfile.NamedTemporaryFile(prefix="TmP") as tmp_file: 39 | temp_dir = os.path.dirname(tmp_file.name) 40 | dest = os.path.join(temp_dir, f"{tmp_file.name}-{'b'}") 41 | try: 42 | os.symlink(tmp_file.name, dest) 43 | can = True 44 | except (OSError, NotImplementedError): 45 | pass 46 | LOGGER.debug("symlink on filesystem does%s work", "" if can else " not") 47 | else: 48 | can = True 49 | _CAN_SYMLINK = can 50 | return _CAN_SYMLINK 51 | 52 | 53 | def fs_path_id(path: str) -> str: 54 | return path.casefold() if fs_is_case_sensitive() else path 55 | 56 | 57 | __all__ = ( 58 | "IS_CPYTHON", 59 | "IS_GRAALPY", 60 | "IS_MAC_ARM64", 61 | "IS_PYPY", 62 | "IS_WIN", 63 | "IS_ZIPAPP", 64 | "ROOT", 65 | "fs_is_case_sensitive", 66 | "fs_path_id", 67 | "fs_supports_symlink", 68 | ) 69 | -------------------------------------------------------------------------------- /src/virtualenv/report.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import sys 5 | 6 | LEVELS = { 7 | 0: logging.CRITICAL, 8 | 1: logging.ERROR, 9 | 2: logging.WARNING, 10 | 3: logging.INFO, 11 | 4: logging.DEBUG, 12 | 5: logging.NOTSET, 13 | } 14 | 15 | MAX_LEVEL = max(LEVELS.keys()) 16 | LOGGER = logging.getLogger() 17 | 18 | 19 | def setup_report(verbosity, show_pid=False): # noqa: FBT002 20 | _clean_handlers(LOGGER) 21 | verbosity = min(verbosity, MAX_LEVEL) # pragma: no cover 22 | level = LEVELS[verbosity] 23 | msg_format = "%(message)s" 24 | if level <= logging.DEBUG: 25 | locate = "module" 26 | msg_format = f"%(relativeCreated)d {msg_format} [%(levelname)s %({locate})s:%(lineno)d]" 27 | if show_pid: 28 | msg_format = f"[%(process)d] {msg_format}" 29 | formatter = logging.Formatter(msg_format) 30 | stream_handler = logging.StreamHandler(stream=sys.stdout) 31 | stream_handler.setLevel(level) 32 | LOGGER.setLevel(logging.NOTSET) 33 | stream_handler.setFormatter(formatter) 34 | LOGGER.addHandler(stream_handler) 35 | level_name = logging.getLevelName(level) 36 | LOGGER.debug("setup logging to %s", level_name) 37 | logging.getLogger("distlib").setLevel(logging.ERROR) 38 | return verbosity 39 | 40 | 41 | def _clean_handlers(log): 42 | for log_handler in list(log.handlers): # remove handlers of libraries 43 | log.removeHandler(log_handler) 44 | 45 | 46 | __all__ = [ 47 | "LEVELS", 48 | "MAX_LEVEL", 49 | "setup_report", 50 | ] 51 | -------------------------------------------------------------------------------- /src/virtualenv/run/plugin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/run/plugin/__init__.py -------------------------------------------------------------------------------- /src/virtualenv/run/plugin/activators.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentTypeError 4 | from collections import OrderedDict 5 | 6 | from .base import ComponentBuilder 7 | 8 | 9 | class ActivationSelector(ComponentBuilder): 10 | def __init__(self, interpreter, parser) -> None: 11 | self.default = None 12 | possible = OrderedDict( 13 | (k, v) for k, v in self.options("virtualenv.activate").items() if v.supports(interpreter) 14 | ) 15 | super().__init__(interpreter, parser, "activators", possible) 16 | self.parser.description = "options for activation scripts" 17 | self.active = None 18 | 19 | def add_selector_arg_parse(self, name, choices): 20 | self.default = ",".join(choices) 21 | self.parser.add_argument( 22 | f"--{name}", 23 | default=self.default, 24 | metavar="comma_sep_list", 25 | required=False, 26 | help="activators to generate - default is all supported", 27 | type=self._extract_activators, 28 | ) 29 | 30 | def _extract_activators(self, entered_str): 31 | elements = [e.strip() for e in entered_str.split(",") if e.strip()] 32 | missing = [e for e in elements if e not in self.possible] 33 | if missing: 34 | msg = f"the following activators are not available {','.join(missing)}" 35 | raise ArgumentTypeError(msg) 36 | return elements 37 | 38 | def handle_selected_arg_parse(self, options): 39 | selected_activators = ( 40 | self._extract_activators(self.default) if options.activators is self.default else options.activators 41 | ) 42 | self.active = {k: v for k, v in self.possible.items() if k in selected_activators} 43 | self.parser.add_argument( 44 | "--prompt", 45 | dest="prompt", 46 | metavar="prompt", 47 | help=( 48 | "provides an alternative prompt prefix for this environment " 49 | "(value of . means name of the current working directory)" 50 | ), 51 | default=None, 52 | ) 53 | for activator in self.active.values(): 54 | activator.add_parser_arguments(self.parser, self.interpreter) 55 | 56 | def create(self, options): 57 | return [activator_class(options) for activator_class in self.active.values()] 58 | 59 | 60 | __all__ = [ 61 | "ActivationSelector", 62 | ] 63 | -------------------------------------------------------------------------------- /src/virtualenv/run/plugin/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from collections import OrderedDict 5 | from importlib.metadata import entry_points 6 | 7 | importlib_metadata_version = () 8 | 9 | 10 | class PluginLoader: 11 | _OPTIONS = None 12 | _ENTRY_POINTS = None 13 | 14 | @classmethod 15 | def entry_points_for(cls, key): 16 | if sys.version_info >= (3, 10) or importlib_metadata_version >= (3, 6): 17 | return OrderedDict((e.name, e.load()) for e in cls.entry_points().select(group=key)) 18 | return OrderedDict((e.name, e.load()) for e in cls.entry_points().get(key, {})) 19 | 20 | @staticmethod 21 | def entry_points(): 22 | if PluginLoader._ENTRY_POINTS is None: 23 | PluginLoader._ENTRY_POINTS = entry_points() 24 | return PluginLoader._ENTRY_POINTS 25 | 26 | 27 | class ComponentBuilder(PluginLoader): 28 | def __init__(self, interpreter, parser, name, possible) -> None: 29 | self.interpreter = interpreter 30 | self.name = name 31 | self._impl_class = None 32 | self.possible = possible 33 | self.parser = parser.add_argument_group(title=name) 34 | self.add_selector_arg_parse(name, list(self.possible)) 35 | 36 | @classmethod 37 | def options(cls, key): 38 | if cls._OPTIONS is None: 39 | cls._OPTIONS = cls.entry_points_for(key) 40 | return cls._OPTIONS 41 | 42 | def add_selector_arg_parse(self, name, choices): 43 | raise NotImplementedError 44 | 45 | def handle_selected_arg_parse(self, options): 46 | selected = getattr(options, self.name) 47 | if selected not in self.possible: 48 | msg = f"No implementation for {self.interpreter}" 49 | raise RuntimeError(msg) 50 | self._impl_class = self.possible[selected] 51 | self.populate_selected_argparse(selected, options.app_data) 52 | return selected 53 | 54 | def populate_selected_argparse(self, selected, app_data): 55 | self.parser.description = f"options for {self.name} {selected}" 56 | self._impl_class.add_parser_arguments(self.parser, self.interpreter, app_data) 57 | 58 | def create(self, options): 59 | return self._impl_class(options, self.interpreter) 60 | 61 | 62 | __all__ = [ 63 | "ComponentBuilder", 64 | "PluginLoader", 65 | ] 66 | -------------------------------------------------------------------------------- /src/virtualenv/run/plugin/creators.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import OrderedDict, defaultdict 4 | from typing import TYPE_CHECKING, NamedTuple 5 | 6 | from virtualenv.create.describe import Describe 7 | from virtualenv.create.via_global_ref.builtin.builtin_way import VirtualenvBuiltin 8 | 9 | from .base import ComponentBuilder 10 | 11 | if TYPE_CHECKING: 12 | from virtualenv.create.creator import Creator, CreatorMeta 13 | 14 | 15 | class CreatorInfo(NamedTuple): 16 | key_to_class: dict[str, type[Creator]] 17 | key_to_meta: dict[str, CreatorMeta] 18 | describe: type[Describe] | None 19 | builtin_key: str 20 | 21 | 22 | class CreatorSelector(ComponentBuilder): 23 | def __init__(self, interpreter, parser) -> None: 24 | creators, self.key_to_meta, self.describe, self.builtin_key = self.for_interpreter(interpreter) 25 | super().__init__(interpreter, parser, "creator", creators) 26 | 27 | @classmethod 28 | def for_interpreter(cls, interpreter): 29 | key_to_class, key_to_meta, builtin_key, describe = OrderedDict(), {}, None, None 30 | errors = defaultdict(list) 31 | for key, creator_class in cls.options("virtualenv.create").items(): 32 | if key == "builtin": 33 | msg = "builtin creator is a reserved name" 34 | raise RuntimeError(msg) 35 | meta = creator_class.can_create(interpreter) 36 | if meta: 37 | if meta.error: 38 | errors[meta.error].append(creator_class) 39 | else: 40 | if "builtin" not in key_to_class and issubclass(creator_class, VirtualenvBuiltin): 41 | builtin_key = key 42 | key_to_class["builtin"] = creator_class 43 | key_to_meta["builtin"] = meta 44 | key_to_class[key] = creator_class 45 | key_to_meta[key] = meta 46 | if describe is None and issubclass(creator_class, Describe) and creator_class.can_describe(interpreter): 47 | describe = creator_class 48 | if not key_to_meta: 49 | if errors: 50 | rows = [f"{k} for creators {', '.join(i.__name__ for i in v)}" for k, v in errors.items()] 51 | raise RuntimeError("\n".join(rows)) 52 | msg = f"No virtualenv implementation for {interpreter}" 53 | raise RuntimeError(msg) 54 | return CreatorInfo( 55 | key_to_class=key_to_class, 56 | key_to_meta=key_to_meta, 57 | describe=describe, 58 | builtin_key=builtin_key, 59 | ) 60 | 61 | def add_selector_arg_parse(self, name, choices): 62 | # prefer the built-in venv if present, otherwise fallback to first defined type 63 | choices = sorted(choices, key=lambda a: 0 if a == "builtin" else 1) 64 | default_value = self._get_default(choices) 65 | self.parser.add_argument( 66 | f"--{name}", 67 | choices=choices, 68 | default=default_value, 69 | required=False, 70 | help=f"create environment via{'' if self.builtin_key is None else f' (builtin = {self.builtin_key})'}", 71 | ) 72 | 73 | @staticmethod 74 | def _get_default(choices): 75 | return next(iter(choices)) 76 | 77 | def populate_selected_argparse(self, selected, app_data): 78 | self.parser.description = f"options for {self.name} {selected}" 79 | self._impl_class.add_parser_arguments(self.parser, self.interpreter, self.key_to_meta[selected], app_data) 80 | 81 | def create(self, options): 82 | options.meta = self.key_to_meta[getattr(options, self.name)] 83 | if not issubclass(self._impl_class, Describe): 84 | options.describe = self.describe(options, self.interpreter) 85 | return super().create(options) 86 | 87 | 88 | __all__ = [ 89 | "CreatorInfo", 90 | "CreatorSelector", 91 | ] 92 | -------------------------------------------------------------------------------- /src/virtualenv/run/plugin/discovery.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .base import PluginLoader 4 | 5 | 6 | class Discovery(PluginLoader): 7 | """Discovery plugins.""" 8 | 9 | 10 | def get_discover(parser, args): 11 | discover_types = Discovery.entry_points_for("virtualenv.discovery") 12 | discovery_parser = parser.add_argument_group( 13 | title="discovery", 14 | description="discover and provide a target interpreter", 15 | ) 16 | choices = _get_default_discovery(discover_types) 17 | # prefer the builtin if present, otherwise fallback to first defined type 18 | choices = sorted(choices, key=lambda a: 0 if a == "builtin" else 1) 19 | discovery_parser.add_argument( 20 | "--discovery", 21 | choices=choices, 22 | default=next(iter(choices)), 23 | required=False, 24 | help="interpreter discovery method", 25 | ) 26 | options, _ = parser.parse_known_args(args) 27 | discover_class = discover_types[options.discovery] 28 | discover_class.add_parser_arguments(discovery_parser) 29 | options, _ = parser.parse_known_args(args, namespace=options) 30 | return discover_class(options) 31 | 32 | 33 | def _get_default_discovery(discover_types): 34 | return list(discover_types.keys()) 35 | 36 | 37 | __all__ = [ 38 | "Discovery", 39 | "get_discover", 40 | ] 41 | -------------------------------------------------------------------------------- /src/virtualenv/run/plugin/seeders.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .base import ComponentBuilder 4 | 5 | 6 | class SeederSelector(ComponentBuilder): 7 | def __init__(self, interpreter, parser) -> None: 8 | possible = self.options("virtualenv.seed") 9 | super().__init__(interpreter, parser, "seeder", possible) 10 | 11 | def add_selector_arg_parse(self, name, choices): 12 | self.parser.add_argument( 13 | f"--{name}", 14 | choices=choices, 15 | default=self._get_default(), 16 | required=False, 17 | help="seed packages install method", 18 | ) 19 | self.parser.add_argument( 20 | "--no-seed", 21 | "--without-pip", 22 | help="do not install seed packages", 23 | action="store_true", 24 | dest="no_seed", 25 | ) 26 | 27 | @staticmethod 28 | def _get_default(): 29 | return "app-data" 30 | 31 | def handle_selected_arg_parse(self, options): 32 | return super().handle_selected_arg_parse(options) 33 | 34 | def create(self, options): 35 | return self._impl_class(options) 36 | 37 | 38 | __all__ = [ 39 | "SeederSelector", 40 | ] 41 | -------------------------------------------------------------------------------- /src/virtualenv/run/session.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import logging 5 | 6 | LOGGER = logging.getLogger(__name__) 7 | 8 | 9 | class Session: 10 | """Represents a virtual environment creation session.""" 11 | 12 | def __init__(self, verbosity, app_data, interpreter, creator, seeder, activators) -> None: # noqa: PLR0913 13 | self._verbosity = verbosity 14 | self._app_data = app_data 15 | self._interpreter = interpreter 16 | self._creator = creator 17 | self._seeder = seeder 18 | self._activators = activators 19 | 20 | @property 21 | def verbosity(self): 22 | """The verbosity of the run.""" 23 | return self._verbosity 24 | 25 | @property 26 | def interpreter(self): 27 | """Create a virtual environment based on this reference interpreter.""" 28 | return self._interpreter 29 | 30 | @property 31 | def creator(self): 32 | """The creator used to build the virtual environment (must be compatible with the interpreter).""" 33 | return self._creator 34 | 35 | @property 36 | def seeder(self): 37 | """The mechanism used to provide the seed packages (pip, setuptools, wheel).""" 38 | return self._seeder 39 | 40 | @property 41 | def activators(self): 42 | """Activators used to generate activations scripts.""" 43 | return self._activators 44 | 45 | def run(self): 46 | self._create() 47 | self._seed() 48 | self._activate() 49 | self.creator.pyenv_cfg.write() 50 | 51 | def _create(self): 52 | LOGGER.info("create virtual environment via %s", self.creator) 53 | self.creator.run() 54 | LOGGER.debug(_DEBUG_MARKER) 55 | LOGGER.debug("%s", _Debug(self.creator)) 56 | 57 | def _seed(self): 58 | if self.seeder is not None and self.seeder.enabled: 59 | LOGGER.info("add seed packages via %s", self.seeder) 60 | self.seeder.run(self.creator) 61 | 62 | def _activate(self): 63 | if self.activators: 64 | active = ", ".join(type(i).__name__.replace("Activator", "") for i in self.activators) 65 | LOGGER.info("add activators for %s", active) 66 | for activator in self.activators: 67 | activator.generate(self.creator) 68 | 69 | def __enter__(self): 70 | return self 71 | 72 | def __exit__(self, exc_type, exc_val, exc_tb): 73 | self._app_data.close() 74 | 75 | 76 | _DEBUG_MARKER = "=" * 30 + " target debug " + "=" * 30 77 | 78 | 79 | class _Debug: 80 | """lazily populate debug.""" 81 | 82 | def __init__(self, creator) -> None: 83 | self.creator = creator 84 | 85 | def __repr__(self) -> str: 86 | return json.dumps(self.creator.debug, indent=2) 87 | 88 | 89 | __all__ = [ 90 | "Session", 91 | ] 92 | -------------------------------------------------------------------------------- /src/virtualenv/seed/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/seed/__init__.py -------------------------------------------------------------------------------- /src/virtualenv/seed/embed/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/seed/embed/__init__.py -------------------------------------------------------------------------------- /src/virtualenv/seed/embed/pip_invoke.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from contextlib import contextmanager 5 | from subprocess import Popen 6 | 7 | from virtualenv.discovery.cached_py_info import LogCmd 8 | from virtualenv.seed.embed.base_embed import BaseEmbed 9 | from virtualenv.seed.wheels import Version, get_wheel, pip_wheel_env_run 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | class PipInvoke(BaseEmbed): 15 | def __init__(self, options) -> None: 16 | super().__init__(options) 17 | 18 | def run(self, creator): 19 | if not self.enabled: 20 | return 21 | for_py_version = creator.interpreter.version_release_str 22 | with self.get_pip_install_cmd(creator.exe, for_py_version) as cmd: 23 | env = pip_wheel_env_run(self.extra_search_dir, self.app_data, self.env) 24 | self._execute(cmd, env) 25 | 26 | @staticmethod 27 | def _execute(cmd, env): 28 | LOGGER.debug("pip seed by running: %s", LogCmd(cmd, env)) 29 | process = Popen(cmd, env=env) 30 | process.communicate() 31 | if process.returncode != 0: 32 | msg = f"failed seed with code {process.returncode}" 33 | raise RuntimeError(msg) 34 | return process 35 | 36 | @contextmanager 37 | def get_pip_install_cmd(self, exe, for_py_version): 38 | cmd = [str(exe), "-m", "pip", "-q", "install", "--only-binary", ":all:", "--disable-pip-version-check"] 39 | if not self.download: 40 | cmd.append("--no-index") 41 | folders = set() 42 | for dist, version in self.distribution_to_versions().items(): 43 | wheel = get_wheel( 44 | distribution=dist, 45 | version=version, 46 | for_py_version=for_py_version, 47 | search_dirs=self.extra_search_dir, 48 | download=False, 49 | app_data=self.app_data, 50 | do_periodic_update=self.periodic_update, 51 | env=self.env, 52 | ) 53 | if wheel is None: 54 | msg = f"could not get wheel for distribution {dist}" 55 | raise RuntimeError(msg) 56 | folders.add(str(wheel.path.parent)) 57 | cmd.append(Version.as_pip_req(dist, wheel.version)) 58 | for folder in sorted(folders): 59 | cmd.extend(["--find-links", str(folder)]) 60 | yield cmd 61 | 62 | 63 | __all__ = [ 64 | "PipInvoke", 65 | ] 66 | -------------------------------------------------------------------------------- /src/virtualenv/seed/embed/via_app_data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/seed/embed/via_app_data/__init__.py -------------------------------------------------------------------------------- /src/virtualenv/seed/embed/via_app_data/pip_install/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/seed/embed/via_app_data/pip_install/__init__.py -------------------------------------------------------------------------------- /src/virtualenv/seed/embed/via_app_data/pip_install/copy.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from pathlib import Path 5 | 6 | from virtualenv.util.path import copy 7 | 8 | from .base import PipInstall 9 | 10 | 11 | class CopyPipInstall(PipInstall): 12 | def _sync(self, src, dst): 13 | copy(src, dst) 14 | 15 | def _generate_new_files(self): 16 | # create the pyc files 17 | new_files = super()._generate_new_files() 18 | new_files.update(self._cache_files()) 19 | return new_files 20 | 21 | def _cache_files(self): 22 | version = self._creator.interpreter.version_info 23 | py_c_ext = f".{self._creator.interpreter.implementation.lower()}-{version.major}{version.minor}.pyc" 24 | for root, dirs, files in os.walk(str(self._image_dir), topdown=True): 25 | root_path = Path(root) 26 | for name in files: 27 | if name.endswith(".py"): 28 | yield root_path / f"{name[:-3]}{py_c_ext}" 29 | for name in dirs: 30 | yield root_path / name / "__pycache__" 31 | 32 | def _fix_records(self, new_files): 33 | extra_record_data_str = self._records_text(new_files) 34 | with (self._dist_info / "RECORD").open("ab") as file_handler: 35 | file_handler.write(extra_record_data_str.encode("utf-8")) 36 | 37 | 38 | __all__ = [ 39 | "CopyPipInstall", 40 | ] 41 | -------------------------------------------------------------------------------- /src/virtualenv/seed/embed/via_app_data/pip_install/symlink.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from stat import S_IREAD, S_IRGRP, S_IROTH 5 | from subprocess import PIPE, Popen 6 | 7 | from virtualenv.util.path import safe_delete, set_tree 8 | 9 | from .base import PipInstall 10 | 11 | 12 | class SymlinkPipInstall(PipInstall): 13 | def _sync(self, src, dst): 14 | os.symlink(str(src), str(dst)) 15 | 16 | def _generate_new_files(self): 17 | # create the pyc files, as the build image will be R/O 18 | cmd = [str(self._creator.exe), "-m", "compileall", str(self._image_dir)] 19 | process = Popen(cmd, stdout=PIPE, stderr=PIPE) 20 | process.communicate() 21 | # the root pyc is shared, so we'll not symlink that - but still add the pyc files to the RECORD for close 22 | root_py_cache = self._image_dir / "__pycache__" 23 | new_files = set() 24 | if root_py_cache.exists(): 25 | new_files.update(root_py_cache.iterdir()) 26 | new_files.add(root_py_cache) 27 | safe_delete(root_py_cache) 28 | core_new_files = super()._generate_new_files() 29 | # remove files that are within the image folder deeper than one level (as these will be not linked directly) 30 | for file in core_new_files: 31 | try: 32 | rel = file.relative_to(self._image_dir) 33 | if len(rel.parts) > 1: 34 | continue 35 | except ValueError: 36 | pass 37 | new_files.add(file) 38 | return new_files 39 | 40 | def _fix_records(self, new_files): 41 | new_files.update(i for i in self._image_dir.iterdir()) 42 | extra_record_data_str = self._records_text(sorted(new_files, key=str)) 43 | (self._dist_info / "RECORD").write_text(extra_record_data_str, encoding="utf-8") 44 | 45 | def build_image(self): 46 | super().build_image() 47 | # protect the image by making it read only 48 | set_tree(self._image_dir, S_IREAD | S_IRGRP | S_IROTH) 49 | 50 | def clear(self): 51 | if self._image_dir.exists(): 52 | safe_delete(self._image_dir) 53 | super().clear() 54 | 55 | 56 | __all__ = [ 57 | "SymlinkPipInstall", 58 | ] 59 | -------------------------------------------------------------------------------- /src/virtualenv/seed/seeder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | 5 | 6 | class Seeder(ABC): 7 | """A seeder will install some seed packages into a virtual environment.""" 8 | 9 | def __init__(self, options, enabled) -> None: 10 | """ 11 | Create. 12 | 13 | :param options: the parsed options as defined within :meth:`add_parser_arguments` 14 | :param enabled: a flag weather the seeder is enabled or not 15 | """ 16 | self.enabled = enabled 17 | self.env = options.env 18 | 19 | @classmethod 20 | def add_parser_arguments(cls, parser, interpreter, app_data): 21 | """ 22 | Add CLI arguments for this seed mechanisms. 23 | 24 | :param parser: the CLI parser 25 | :param app_data: the CLI parser 26 | :param interpreter: the interpreter this virtual environment is based of 27 | """ 28 | raise NotImplementedError 29 | 30 | @abstractmethod 31 | def run(self, creator): 32 | """ 33 | Perform the seed operation. 34 | 35 | :param creator: the creator (based of :class:`virtualenv.create.creator.Creator`) we used to create this \ 36 | virtual environment 37 | """ 38 | raise NotImplementedError 39 | 40 | 41 | __all__ = [ 42 | "Seeder", 43 | ] 44 | -------------------------------------------------------------------------------- /src/virtualenv/seed/wheels/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .acquire import get_wheel, pip_wheel_env_run 4 | from .util import Version, Wheel 5 | 6 | __all__ = [ 7 | "Version", 8 | "Wheel", 9 | "get_wheel", 10 | "pip_wheel_env_run", 11 | ] 12 | -------------------------------------------------------------------------------- /src/virtualenv/seed/wheels/bundle.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from virtualenv.seed.wheels.embed import get_embed_wheel 4 | 5 | from .periodic_update import periodic_update 6 | from .util import Version, Wheel, discover_wheels 7 | 8 | 9 | def from_bundle(distribution, version, for_py_version, search_dirs, app_data, do_periodic_update, env): # noqa: PLR0913 10 | """Load the bundled wheel to a cache directory.""" 11 | of_version = Version.of_version(version) 12 | wheel = load_embed_wheel(app_data, distribution, for_py_version, of_version) 13 | 14 | if version != Version.embed: 15 | # 2. check if we have upgraded embed 16 | if app_data.can_update: 17 | per = do_periodic_update 18 | wheel = periodic_update(distribution, of_version, for_py_version, wheel, search_dirs, app_data, per, env) 19 | 20 | # 3. acquire from extra search dir 21 | found_wheel = from_dir(distribution, of_version, for_py_version, search_dirs) 22 | if found_wheel is not None and (wheel is None or found_wheel.version_tuple > wheel.version_tuple): 23 | wheel = found_wheel 24 | return wheel 25 | 26 | 27 | def load_embed_wheel(app_data, distribution, for_py_version, version): 28 | wheel = get_embed_wheel(distribution, for_py_version) 29 | if wheel is not None: 30 | version_match = version == wheel.version 31 | if version is None or version_match: 32 | with app_data.ensure_extracted(wheel.path, lambda: app_data.house) as wheel_path: 33 | wheel = Wheel(wheel_path) 34 | else: # if version does not match ignore 35 | wheel = None 36 | return wheel 37 | 38 | 39 | def from_dir(distribution, version, for_py_version, directories): 40 | """Load a compatible wheel from a given folder.""" 41 | for folder in directories: 42 | for wheel in discover_wheels(folder, distribution, version, for_py_version): 43 | return wheel 44 | return None 45 | 46 | 47 | __all__ = [ 48 | "from_bundle", 49 | "load_embed_wheel", 50 | ] 51 | -------------------------------------------------------------------------------- /src/virtualenv/seed/wheels/embed/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from virtualenv.seed.wheels.util import Wheel 6 | 7 | BUNDLE_FOLDER = Path(__file__).absolute().parent 8 | BUNDLE_SUPPORT = { 9 | "3.8": { 10 | "pip": "pip-25.0.1-py3-none-any.whl", 11 | "setuptools": "setuptools-75.3.2-py3-none-any.whl", 12 | "wheel": "wheel-0.45.1-py3-none-any.whl", 13 | }, 14 | "3.9": { 15 | "pip": "pip-25.1.1-py3-none-any.whl", 16 | "setuptools": "setuptools-80.3.1-py3-none-any.whl", 17 | }, 18 | "3.10": { 19 | "pip": "pip-25.1.1-py3-none-any.whl", 20 | "setuptools": "setuptools-80.3.1-py3-none-any.whl", 21 | }, 22 | "3.11": { 23 | "pip": "pip-25.1.1-py3-none-any.whl", 24 | "setuptools": "setuptools-80.3.1-py3-none-any.whl", 25 | }, 26 | "3.12": { 27 | "pip": "pip-25.1.1-py3-none-any.whl", 28 | "setuptools": "setuptools-80.3.1-py3-none-any.whl", 29 | }, 30 | "3.13": { 31 | "pip": "pip-25.1.1-py3-none-any.whl", 32 | "setuptools": "setuptools-80.3.1-py3-none-any.whl", 33 | }, 34 | "3.14": { 35 | "pip": "pip-25.1.1-py3-none-any.whl", 36 | "setuptools": "setuptools-80.3.1-py3-none-any.whl", 37 | }, 38 | } 39 | MAX = "3.8" 40 | 41 | 42 | def get_embed_wheel(distribution, for_py_version): 43 | mapping = BUNDLE_SUPPORT.get(for_py_version, {}) or BUNDLE_SUPPORT[MAX] 44 | wheel_file = mapping.get(distribution) 45 | if wheel_file is None: 46 | return None 47 | path = BUNDLE_FOLDER / wheel_file 48 | return Wheel.from_path(path) 49 | 50 | 51 | __all__ = [ 52 | "BUNDLE_FOLDER", 53 | "BUNDLE_SUPPORT", 54 | "MAX", 55 | "get_embed_wheel", 56 | ] 57 | -------------------------------------------------------------------------------- /src/virtualenv/seed/wheels/embed/pip-25.0.1-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/seed/wheels/embed/pip-25.0.1-py3-none-any.whl -------------------------------------------------------------------------------- /src/virtualenv/seed/wheels/embed/pip-25.1.1-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/seed/wheels/embed/pip-25.1.1-py3-none-any.whl -------------------------------------------------------------------------------- /src/virtualenv/seed/wheels/embed/setuptools-75.3.2-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/seed/wheels/embed/setuptools-75.3.2-py3-none-any.whl -------------------------------------------------------------------------------- /src/virtualenv/seed/wheels/embed/setuptools-80.3.1-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/seed/wheels/embed/setuptools-80.3.1-py3-none-any.whl -------------------------------------------------------------------------------- /src/virtualenv/seed/wheels/embed/wheel-0.45.1-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/seed/wheels/embed/wheel-0.45.1-py3-none-any.whl -------------------------------------------------------------------------------- /src/virtualenv/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/src/virtualenv/util/__init__.py -------------------------------------------------------------------------------- /src/virtualenv/util/error.py: -------------------------------------------------------------------------------- 1 | """Errors.""" 2 | 3 | from __future__ import annotations 4 | 5 | 6 | class ProcessCallFailedError(RuntimeError): 7 | """Failed a process call.""" 8 | 9 | def __init__(self, code, out, err, cmd) -> None: 10 | super().__init__(code, out, err, cmd) 11 | self.code = code 12 | self.out = out 13 | self.err = err 14 | self.cmd = cmd 15 | -------------------------------------------------------------------------------- /src/virtualenv/util/path/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from ._permission import make_exe, set_tree 4 | from ._sync import copy, copytree, ensure_dir, safe_delete, symlink 5 | from ._win import get_short_path_name 6 | 7 | __all__ = [ 8 | "copy", 9 | "copytree", 10 | "ensure_dir", 11 | "get_short_path_name", 12 | "make_exe", 13 | "safe_delete", 14 | "set_tree", 15 | "symlink", 16 | ] 17 | -------------------------------------------------------------------------------- /src/virtualenv/util/path/_permission.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from stat import S_IXGRP, S_IXOTH, S_IXUSR 5 | 6 | 7 | def make_exe(filename): 8 | original_mode = filename.stat().st_mode 9 | levels = [S_IXUSR, S_IXGRP, S_IXOTH] 10 | for at in range(len(levels), 0, -1): 11 | try: 12 | mode = original_mode 13 | for level in levels[:at]: 14 | mode |= level 15 | filename.chmod(mode) 16 | break 17 | except OSError: 18 | continue 19 | 20 | 21 | def set_tree(folder, stat): 22 | for root, _, files in os.walk(str(folder)): 23 | for filename in files: 24 | os.chmod(os.path.join(root, filename), stat) 25 | 26 | 27 | __all__ = ( 28 | "make_exe", 29 | "set_tree", 30 | ) 31 | -------------------------------------------------------------------------------- /src/virtualenv/util/path/_sync.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os 5 | import shutil 6 | import sys 7 | from stat import S_IWUSR 8 | 9 | LOGGER = logging.getLogger(__name__) 10 | 11 | 12 | def ensure_dir(path): 13 | if not path.exists(): 14 | LOGGER.debug("create folder %s", str(path)) 15 | os.makedirs(str(path)) 16 | 17 | 18 | def ensure_safe_to_do(src, dest): 19 | if src == dest: 20 | msg = f"source and destination is the same {src}" 21 | raise ValueError(msg) 22 | if not dest.exists(): 23 | return 24 | if dest.is_dir() and not dest.is_symlink(): 25 | LOGGER.debug("remove directory %s", dest) 26 | safe_delete(dest) 27 | else: 28 | LOGGER.debug("remove file %s", dest) 29 | dest.unlink() 30 | 31 | 32 | def symlink(src, dest): 33 | ensure_safe_to_do(src, dest) 34 | LOGGER.debug("symlink %s", _Debug(src, dest)) 35 | dest.symlink_to(src, target_is_directory=src.is_dir()) 36 | 37 | 38 | def copy(src, dest): 39 | ensure_safe_to_do(src, dest) 40 | is_dir = src.is_dir() 41 | method = copytree if is_dir else shutil.copy 42 | LOGGER.debug("copy %s", _Debug(src, dest)) 43 | method(str(src), str(dest)) 44 | 45 | 46 | def copytree(src, dest): 47 | for root, _, files in os.walk(src): 48 | dest_dir = os.path.join(dest, os.path.relpath(root, src)) 49 | if not os.path.isdir(dest_dir): 50 | os.makedirs(dest_dir) 51 | for name in files: 52 | src_f = os.path.join(root, name) 53 | dest_f = os.path.join(dest_dir, name) 54 | shutil.copy(src_f, dest_f) 55 | 56 | 57 | def safe_delete(dest): 58 | def onerror(func, path, exc_info): # noqa: ARG001 59 | if not os.access(path, os.W_OK): 60 | os.chmod(path, S_IWUSR) 61 | func(path) 62 | else: 63 | raise # noqa: PLE0704 64 | 65 | kwargs = {"onexc" if sys.version_info >= (3, 12) else "onerror": onerror} 66 | shutil.rmtree(str(dest), ignore_errors=True, **kwargs) 67 | 68 | 69 | class _Debug: 70 | def __init__(self, src, dest) -> None: 71 | self.src = src 72 | self.dest = dest 73 | 74 | def __str__(self) -> str: 75 | return f"{'directory ' if self.src.is_dir() else ''}{self.src!s} to {self.dest!s}" 76 | 77 | 78 | __all__ = [ 79 | "copy", 80 | "copytree", 81 | "ensure_dir", 82 | "safe_delete", 83 | "symlink", 84 | "symlink", 85 | ] 86 | -------------------------------------------------------------------------------- /src/virtualenv/util/path/_win.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | def get_short_path_name(long_name): 5 | """Gets the short path name of a given long path - http://stackoverflow.com/a/23598461/200291.""" 6 | import ctypes # noqa: PLC0415 7 | from ctypes import wintypes # noqa: PLC0415 8 | 9 | GetShortPathNameW = ctypes.windll.kernel32.GetShortPathNameW # noqa: N806 10 | GetShortPathNameW.argtypes = [wintypes.LPCWSTR, wintypes.LPWSTR, wintypes.DWORD] 11 | GetShortPathNameW.restype = wintypes.DWORD 12 | output_buf_size = 0 13 | while True: 14 | output_buf = ctypes.create_unicode_buffer(output_buf_size) 15 | needed = GetShortPathNameW(long_name, output_buf, output_buf_size) 16 | if output_buf_size >= needed: 17 | return output_buf.value 18 | output_buf_size = needed 19 | 20 | 21 | __all__ = [ 22 | "get_short_path_name", 23 | ] 24 | -------------------------------------------------------------------------------- /src/virtualenv/util/subprocess/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import subprocess 4 | 5 | CREATE_NO_WINDOW = 0x80000000 6 | 7 | 8 | def run_cmd(cmd): 9 | try: 10 | process = subprocess.Popen( 11 | cmd, 12 | universal_newlines=True, 13 | stdin=subprocess.PIPE, 14 | stderr=subprocess.PIPE, 15 | stdout=subprocess.PIPE, 16 | encoding="utf-8", 17 | ) 18 | out, err = process.communicate() # input disabled 19 | code = process.returncode 20 | except OSError as error: 21 | code, out, err = error.errno, "", error.strerror 22 | if code == 2 and "file" in err: # noqa: PLR2004 23 | err = str(error) # FileNotFoundError in Python >= 3.3 24 | return code, out, err 25 | 26 | 27 | __all__ = ( 28 | "CREATE_NO_WINDOW", 29 | "run_cmd", 30 | ) 31 | -------------------------------------------------------------------------------- /src/virtualenv/util/zipapp.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os 5 | import zipfile 6 | 7 | from virtualenv.info import IS_WIN, ROOT 8 | 9 | LOGGER = logging.getLogger(__name__) 10 | 11 | 12 | def read(full_path): 13 | sub_file = _get_path_within_zip(full_path) 14 | with zipfile.ZipFile(ROOT, "r") as zip_file, zip_file.open(sub_file) as file_handler: 15 | return file_handler.read().decode("utf-8") 16 | 17 | 18 | def extract(full_path, dest): 19 | LOGGER.debug("extract %s to %s", full_path, dest) 20 | sub_file = _get_path_within_zip(full_path) 21 | with zipfile.ZipFile(ROOT, "r") as zip_file: 22 | info = zip_file.getinfo(sub_file) 23 | info.filename = dest.name 24 | zip_file.extract(info, str(dest.parent)) 25 | 26 | 27 | def _get_path_within_zip(full_path): 28 | full_path = os.path.realpath(os.path.abspath(str(full_path))) 29 | prefix = f"{ROOT}{os.sep}" 30 | if not full_path.startswith(prefix): 31 | msg = f"full_path={full_path} should start with prefix={prefix}." 32 | raise RuntimeError(msg) 33 | sub_file = full_path[len(prefix) :] 34 | if IS_WIN: 35 | # paths are always UNIX separators, even on Windows, though __file__ still follows platform default 36 | sub_file = sub_file.replace(os.sep, "/") 37 | return sub_file 38 | 39 | 40 | __all__ = [ 41 | "extract", 42 | "read", 43 | ] 44 | -------------------------------------------------------------------------------- /tasks/pick_tox_env.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import sys 5 | from pathlib import Path 6 | 7 | py = sys.argv[1] 8 | if py.startswith("brew@"): 9 | py = py[len("brew@") :] 10 | if py.startswith("graalpy-"): 11 | py = "graalpy" 12 | env = f"TOXENV={py}" 13 | if len(sys.argv) > 2: # noqa: PLR2004 14 | env += f"\nTOX_BASEPYTHON={sys.argv[2]}" 15 | with Path(os.environ["GITHUB_ENV"]).open("ta", encoding="utf-8") as file_handler: 16 | file_handler.write(env) 17 | -------------------------------------------------------------------------------- /tasks/release.py: -------------------------------------------------------------------------------- 1 | """Handles creating a release PR.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pathlib import Path 6 | from subprocess import check_call 7 | 8 | from git import Commit, Head, Remote, Repo, TagReference 9 | from packaging.version import Version 10 | 11 | ROOT_SRC_DIR = Path(__file__).resolve().parents[1] 12 | 13 | 14 | def main(version_str: str) -> None: 15 | version = Version(version_str) 16 | repo = Repo(str(ROOT_SRC_DIR)) 17 | 18 | if repo.is_dirty(): 19 | msg = "Current repository is dirty. Please commit any changes and try again." 20 | raise RuntimeError(msg) 21 | upstream, release_branch = create_release_branch(repo, version) 22 | release_commit = release_changelog(repo, version) 23 | tag = tag_release_commit(release_commit, repo, version) 24 | print("push release commit") # noqa: T201 25 | repo.git.push(upstream.name, release_branch) 26 | print("push release tag") # noqa: T201 27 | repo.git.push(upstream.name, tag) 28 | print("All done! ✨ 🍰 ✨") # noqa: T201 29 | 30 | 31 | def create_release_branch(repo: Repo, version: Version) -> tuple[Remote, Head]: 32 | print("create release branch from upstream main") # noqa: T201 33 | upstream = get_upstream(repo) 34 | upstream.fetch() 35 | branch_name = f"release-{version}" 36 | release_branch = repo.create_head(branch_name, upstream.refs.main, force=True) 37 | upstream.push(refspec=f"{branch_name}:{branch_name}", force=True) 38 | release_branch.set_tracking_branch(repo.refs[f"{upstream.name}/{branch_name}"]) 39 | release_branch.checkout() 40 | return upstream, release_branch 41 | 42 | 43 | def get_upstream(repo: Repo) -> Remote: 44 | upstream_remote = "pypa/virtualenv.git" 45 | urls = set() 46 | for remote in repo.remotes: 47 | for url in remote.urls: 48 | if url.endswith(upstream_remote): 49 | return remote 50 | urls.add(url) 51 | msg = f"could not find {upstream_remote} remote, has {urls}" 52 | raise RuntimeError(msg) 53 | 54 | 55 | def release_changelog(repo: Repo, version: Version) -> Commit: 56 | print("generate release commit") # noqa: T201 57 | check_call(["towncrier", "build", "--yes", "--version", version.public], cwd=str(ROOT_SRC_DIR)) # noqa: S607 58 | return repo.index.commit(f"release {version}") 59 | 60 | 61 | def tag_release_commit(release_commit, repo, version) -> TagReference: 62 | print("tag release commit") # noqa: T201 63 | existing_tags = [x.name for x in repo.tags] 64 | if version in existing_tags: 65 | print(f"delete existing tag {version}") # noqa: T201 66 | repo.delete_tag(version) 67 | print(f"create tag {version}") # noqa: T201 68 | return repo.create_tag(version, ref=release_commit, force=True) 69 | 70 | 71 | if __name__ == "__main__": 72 | import argparse 73 | 74 | parser = argparse.ArgumentParser(prog="release") 75 | parser.add_argument("--version", required=True) 76 | options = parser.parse_args() 77 | main(options.version) 78 | -------------------------------------------------------------------------------- /tasks/update_embedded.py: -------------------------------------------------------------------------------- 1 | """Helper script to rebuild virtualenv.py from virtualenv_support.""" # noqa: EXE002 2 | 3 | from __future__ import annotations 4 | 5 | import codecs 6 | import locale 7 | import os 8 | import re 9 | from zlib import crc32 as _crc32 10 | 11 | 12 | def crc32(data): 13 | """Python version idempotent.""" 14 | return _crc32(data.encode()) & 0xFFFFFFFF 15 | 16 | 17 | here = os.path.realpath(os.path.dirname(__file__)) 18 | script = os.path.realpath(os.path.join(here, "..", "src", "virtualenv.py")) 19 | 20 | gzip = codecs.lookup("zlib") 21 | b64 = codecs.lookup("base64") 22 | 23 | file_regex = re.compile(r'# file (.*?)\n([a-zA-Z][a-zA-Z0-9_]+) = convert\(\n {4}"""\n(.*?)"""\n\)', re.DOTALL) 24 | file_template = '# file {filename}\n{variable} = convert(\n """\n{data}"""\n)' 25 | 26 | 27 | def rebuild(script_path): 28 | with script_path.open(encoding=locale.getpreferredencoding(False)) as current_fh: # noqa: FBT003 29 | script_content = current_fh.read() 30 | script_parts = [] 31 | match_end = 0 32 | next_match = None 33 | _count, did_update = 0, False 34 | for _count, next_match in enumerate(file_regex.finditer(script_content)): 35 | script_parts += [script_content[match_end : next_match.start()]] 36 | match_end = next_match.end() 37 | filename, variable_name, previous_encoded = next_match.group(1), next_match.group(2), next_match.group(3) 38 | differ, content = handle_file(next_match.group(0), filename, variable_name, previous_encoded) 39 | script_parts.append(content) 40 | if differ: 41 | did_update = True 42 | 43 | script_parts += [script_content[match_end:]] 44 | new_content = "".join(script_parts) 45 | 46 | report(1 if not _count or did_update else 0, new_content, next_match, script_content, script_path) 47 | 48 | 49 | def handle_file(previous_content, filename, variable_name, previous_encoded): 50 | print(f"Found file {filename}") # noqa: T201 51 | current_path = os.path.realpath(os.path.join(here, "..", "src", "virtualenv_embedded", filename)) 52 | _, file_type = os.path.splitext(current_path) 53 | keep_line_ending = file_type == ".bat" 54 | with open(current_path, encoding="utf-8", newline="" if keep_line_ending else None) as current_fh: 55 | current_text = current_fh.read() 56 | current_crc = crc32(current_text) 57 | current_encoded = b64.encode(gzip.encode(current_text.encode())[0])[0].decode() 58 | if current_encoded == previous_encoded: 59 | print(f" File up to date (crc: {current_crc:08x})") # noqa: T201 60 | return False, previous_content 61 | # Else: content has changed 62 | previous_text = gzip.decode(b64.decode(previous_encoded.encode())[0])[0].decode() 63 | previous_crc = crc32(previous_text) 64 | print(f" Content changed (crc: {previous_crc:08x} -> {current_crc:08x})") # noqa: T201 65 | new_part = file_template.format(filename=filename, variable=variable_name, data=current_encoded) 66 | return True, new_part 67 | 68 | 69 | def report(exit_code, new, next_match, current, script_path): 70 | if new != current: 71 | print("Content updated; overwriting... ", end="") # noqa: T201 72 | script_path.write_bytes(new) 73 | print("done.") # noqa: T201 74 | else: 75 | print("No changes in content") # noqa: T201 76 | if next_match is None: 77 | print("No variables were matched/found") # noqa: T201 78 | raise SystemExit(exit_code) 79 | 80 | 81 | if __name__ == "__main__": 82 | rebuild(script) 83 | -------------------------------------------------------------------------------- /tests/integration/test_cachedir_tag.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import shutil 4 | import sys 5 | from subprocess import check_output, run 6 | from typing import TYPE_CHECKING 7 | 8 | import pytest 9 | 10 | from virtualenv import cli_run 11 | 12 | if TYPE_CHECKING: 13 | from pathlib import Path 14 | 15 | # gtar => gnu-tar on macOS 16 | TAR = next((target for target in ("gtar", "tar") if shutil.which(target)), None) 17 | 18 | 19 | def compatible_is_tar_present() -> bool: 20 | return TAR and "--exclude-caches" in check_output(args=[TAR, "--help"], text=True) 21 | 22 | 23 | @pytest.mark.skipif(sys.platform == "win32", reason="Windows does not have tar") 24 | @pytest.mark.skipif(not compatible_is_tar_present(), reason="Compatible tar is not installed") 25 | def test_cachedir_tag_ignored_by_tag(tmp_path: Path) -> None: 26 | venv = tmp_path / ".venv" 27 | cli_run(["--activators", "", "--without-pip", str(venv)]) 28 | 29 | args = [TAR, "--create", "--file", "/dev/null", "--exclude-caches", "--verbose", venv.name] 30 | tar_result = run(args=args, capture_output=True, text=True, cwd=tmp_path) 31 | assert tar_result.stdout == ".venv/\n.venv/CACHEDIR.TAG\n" 32 | assert tar_result.stderr == f"{TAR}: .venv/: contains a cache directory tag CACHEDIR.TAG; contents not dumped\n" 33 | -------------------------------------------------------------------------------- /tests/integration/test_run_int.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | import pytest 6 | 7 | from virtualenv import cli_run 8 | from virtualenv.info import IS_PYPY 9 | from virtualenv.util.subprocess import run_cmd 10 | 11 | if TYPE_CHECKING: 12 | from pathlib import Path 13 | 14 | 15 | @pytest.mark.skipif(IS_PYPY, reason="setuptools distutils patching does not work") 16 | def test_app_data_pinning(tmp_path: Path) -> None: 17 | version = "23.1" 18 | result = cli_run([str(tmp_path), "--pip", version, "--activators", "", "--seeder", "app-data"]) 19 | code, out, _ = run_cmd([str(result.creator.script("pip")), "list", "--disable-pip-version-check"]) 20 | assert not code 21 | for line in out.splitlines(): 22 | parts = line.split() 23 | if parts and parts[0] == "pip": 24 | assert parts[1] == version 25 | break 26 | else: 27 | assert not out 28 | -------------------------------------------------------------------------------- /tests/unit/activation/test_activation_support.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import Namespace 4 | 5 | import pytest 6 | 7 | from virtualenv.activation import ( 8 | BashActivator, 9 | BatchActivator, 10 | CShellActivator, 11 | FishActivator, 12 | PowerShellActivator, 13 | PythonActivator, 14 | ) 15 | from virtualenv.discovery.py_info import PythonInfo 16 | 17 | 18 | @pytest.mark.parametrize( 19 | "activator_class", 20 | [BatchActivator, PowerShellActivator, PythonActivator, BashActivator, FishActivator], 21 | ) 22 | def test_activator_support_windows(mocker, activator_class): 23 | activator = activator_class(Namespace(prompt=None)) 24 | 25 | interpreter = mocker.Mock(spec=PythonInfo) 26 | interpreter.os = "nt" 27 | assert activator.supports(interpreter) 28 | 29 | 30 | @pytest.mark.parametrize("activator_class", [CShellActivator]) 31 | def test_activator_no_support_windows(mocker, activator_class): 32 | activator = activator_class(Namespace(prompt=None)) 33 | 34 | interpreter = mocker.Mock(spec=PythonInfo) 35 | interpreter.os = "nt" 36 | assert not activator.supports(interpreter) 37 | 38 | 39 | @pytest.mark.parametrize( 40 | "activator_class", 41 | [BashActivator, CShellActivator, FishActivator, PowerShellActivator, PythonActivator], 42 | ) 43 | def test_activator_support_posix(mocker, activator_class): 44 | activator = activator_class(Namespace(prompt=None)) 45 | interpreter = mocker.Mock(spec=PythonInfo) 46 | interpreter.os = "posix" 47 | assert activator.supports(interpreter) 48 | 49 | 50 | @pytest.mark.parametrize("activator_class", [BatchActivator]) 51 | def test_activator_no_support_posix(mocker, activator_class): 52 | activator = activator_class(Namespace(prompt=None)) 53 | interpreter = mocker.Mock(spec=PythonInfo) 54 | interpreter.os = "posix" 55 | assert not activator.supports(interpreter) 56 | -------------------------------------------------------------------------------- /tests/unit/activation/test_activator.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import Namespace 4 | 5 | from virtualenv.activation.activator import Activator 6 | 7 | 8 | def test_activator_prompt_cwd(monkeypatch, tmp_path): 9 | class FakeActivator(Activator): 10 | def generate(self, creator): 11 | raise NotImplementedError 12 | 13 | cwd = tmp_path / "magic" 14 | cwd.mkdir() 15 | monkeypatch.chdir(cwd) 16 | 17 | activator = FakeActivator(Namespace(prompt=".")) 18 | assert activator.flag_prompt == "magic" 19 | -------------------------------------------------------------------------------- /tests/unit/activation/test_bash.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from virtualenv.activation import BashActivator 6 | from virtualenv.info import IS_WIN 7 | 8 | 9 | @pytest.mark.skipif(IS_WIN, reason="Github Actions ships with WSL bash") 10 | @pytest.mark.parametrize("hashing_enabled", [True, False]) 11 | def test_bash(raise_on_non_source_class, hashing_enabled, activation_tester): 12 | class Bash(raise_on_non_source_class): 13 | def __init__(self, session) -> None: 14 | super().__init__( 15 | BashActivator, 16 | session, 17 | "bash", 18 | "activate", 19 | "sh", 20 | "You must source this script: $ source ", 21 | ) 22 | self.deactivate += " || exit 1" 23 | self._invoke_script.append("-h" if hashing_enabled else "+h") 24 | 25 | def activate_call(self, script): 26 | return super().activate_call(script) + " || exit 1" 27 | 28 | def print_prompt(self): 29 | return self.print_os_env_var("PS1") 30 | 31 | activation_tester(Bash) 32 | -------------------------------------------------------------------------------- /tests/unit/activation/test_batch.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from virtualenv.activation import BatchActivator 6 | 7 | 8 | @pytest.mark.usefixtures("activation_python") 9 | def test_batch(activation_tester_class, activation_tester, tmp_path): 10 | version_script = tmp_path / "version.bat" 11 | version_script.write_text("ver", encoding="utf-8") 12 | 13 | class Batch(activation_tester_class): 14 | def __init__(self, session) -> None: 15 | super().__init__(BatchActivator, session, None, "activate.bat", "bat") 16 | self._version_cmd = [str(version_script)] 17 | self._invoke_script = [] 18 | self.deactivate = "call deactivate" 19 | self.activate_cmd = "call" 20 | self.pydoc_call = f"call {self.pydoc_call}" 21 | self.unix_line_ending = False 22 | 23 | def _get_test_lines(self, activate_script): 24 | return ["@echo off", *super()._get_test_lines(activate_script)] 25 | 26 | def quote(self, s): 27 | if '"' in s or " " in s: 28 | text = s.replace('"', r"\"") 29 | return f'"{text}"' 30 | return s 31 | 32 | def print_prompt(self): 33 | return 'echo "%PROMPT%"' 34 | 35 | activation_tester(Batch) 36 | 37 | 38 | @pytest.mark.usefixtures("activation_python") 39 | def test_batch_output(activation_tester_class, activation_tester, tmp_path): 40 | version_script = tmp_path / "version.bat" 41 | version_script.write_text("ver", encoding="utf-8") 42 | 43 | class Batch(activation_tester_class): 44 | def __init__(self, session) -> None: 45 | super().__init__(BatchActivator, session, None, "activate.bat", "bat") 46 | self._version_cmd = [str(version_script)] 47 | self._invoke_script = [] 48 | self.deactivate = "call deactivate" 49 | self.activate_cmd = "call" 50 | self.pydoc_call = f"call {self.pydoc_call}" 51 | self.unix_line_ending = False 52 | 53 | def _get_test_lines(self, activate_script): 54 | """ 55 | Build intermediary script which will be then called. 56 | In the script just activate environment, call echo to get current 57 | echo setting, and then deactivate. This ensures that echo setting 58 | is preserved and no unwanted output appears. 59 | """ 60 | intermediary_script_path = str(tmp_path / "intermediary.bat") 61 | activate_script_quoted = self.quote(str(activate_script)) 62 | return [ 63 | "@echo on", 64 | f"@echo @call {activate_script_quoted} > {intermediary_script_path}", 65 | f"@echo @echo >> {intermediary_script_path}", 66 | f"@echo @deactivate >> {intermediary_script_path}", 67 | f"@call {intermediary_script_path}", 68 | ] 69 | 70 | def assert_output(self, out, raw, tmp_path): # noqa: ARG002 71 | assert out[0] == "ECHO is on.", raw 72 | 73 | def quote(self, s): 74 | if '"' in s or " " in s: 75 | text = s.replace('"', r"\"") 76 | return f'"{text}"' 77 | return s 78 | 79 | def print_prompt(self): 80 | return 'echo "%PROMPT%"' 81 | 82 | activation_tester(Batch) 83 | -------------------------------------------------------------------------------- /tests/unit/activation/test_csh.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from shutil import which 5 | from subprocess import check_output 6 | 7 | import pytest 8 | from packaging.version import Version 9 | 10 | from virtualenv.activation import CShellActivator 11 | 12 | 13 | def test_csh(activation_tester_class, activation_tester): 14 | exe = f"tcsh{'.exe' if sys.platform == 'win32' else ''}" 15 | if which(exe): 16 | version_text = check_output([exe, "--version"], text=True, encoding="utf-8") 17 | version = Version(version_text.split(" ")[1]) 18 | if version >= Version("6.24.14"): 19 | pytest.skip("https://github.com/tcsh-org/tcsh/issues/117") 20 | 21 | class Csh(activation_tester_class): 22 | def __init__(self, session) -> None: 23 | super().__init__(CShellActivator, session, "csh", "activate.csh", "csh") 24 | 25 | def print_prompt(self): 26 | # Original csh doesn't print the last newline, 27 | # breaking the test; hence the trailing echo. 28 | return "echo 'source \"$VIRTUAL_ENV/bin/activate.csh\"; echo $prompt' | csh -i ; echo" 29 | 30 | activation_tester(Csh) 31 | -------------------------------------------------------------------------------- /tests/unit/activation/test_fish.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from virtualenv.activation import FishActivator 6 | from virtualenv.info import IS_WIN 7 | 8 | 9 | @pytest.mark.skipif(IS_WIN, reason="we have not setup fish in CI yet") 10 | def test_fish(activation_tester_class, activation_tester, monkeypatch, tmp_path): 11 | monkeypatch.setenv("HOME", str(tmp_path)) 12 | fish_conf_dir = tmp_path / ".config" / "fish" 13 | fish_conf_dir.mkdir(parents=True) 14 | (fish_conf_dir / "config.fish").write_text("", encoding="utf-8") 15 | 16 | class Fish(activation_tester_class): 17 | def __init__(self, session) -> None: 18 | super().__init__(FishActivator, session, "fish", "activate.fish", "fish") 19 | 20 | def print_prompt(self): 21 | return "fish_prompt" 22 | 23 | activation_tester(Fish) 24 | -------------------------------------------------------------------------------- /tests/unit/activation/test_nushell.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from shutil import which 4 | 5 | from virtualenv.activation import NushellActivator 6 | from virtualenv.info import IS_WIN 7 | 8 | 9 | def test_nushell(activation_tester_class, activation_tester): 10 | class Nushell(activation_tester_class): 11 | def __init__(self, session) -> None: 12 | cmd = which("nu") 13 | if cmd is None and IS_WIN: 14 | cmd = "c:\\program files\\nu\\bin\\nu.exe" 15 | 16 | super().__init__(NushellActivator, session, cmd, "activate.nu", "nu") 17 | 18 | self.activate_cmd = "overlay use" 19 | self.unix_line_ending = not IS_WIN 20 | 21 | def print_prompt(self): 22 | return r"print $env.VIRTUAL_PREFIX" 23 | 24 | def activate_call(self, script): 25 | # Commands are called without quotes in Nushell 26 | cmd = self.activate_cmd 27 | scr = self.quote(str(script)) 28 | return f"{cmd} {scr}".strip() 29 | 30 | activation_tester(Nushell) 31 | -------------------------------------------------------------------------------- /tests/unit/activation/test_powershell.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | 5 | import pytest 6 | 7 | from virtualenv.activation import PowerShellActivator 8 | 9 | 10 | @pytest.mark.slow 11 | def test_powershell(activation_tester_class, activation_tester, monkeypatch): 12 | monkeypatch.setenv("TERM", "xterm") 13 | 14 | class PowerShell(activation_tester_class): 15 | def __init__(self, session) -> None: 16 | cmd = "powershell.exe" if sys.platform == "win32" else "pwsh" 17 | super().__init__(PowerShellActivator, session, cmd, "activate.ps1", "ps1") 18 | self._version_cmd = [cmd, "-c", "$PSVersionTable"] 19 | self._invoke_script = [cmd, "-ExecutionPolicy", "ByPass", "-File"] 20 | self.activate_cmd = "." 21 | self.script_encoding = "utf-8-sig" 22 | 23 | def _get_test_lines(self, activate_script): 24 | return super()._get_test_lines(activate_script) 25 | 26 | def invoke_script(self): 27 | return [self.cmd, "-File"] 28 | 29 | def print_prompt(self): 30 | return "prompt" 31 | 32 | def quote(self, s): 33 | """ 34 | Tester will pass strings to native commands on Windows so extra 35 | parsing rules are used. Check `PowerShellActivator.quote` for more 36 | details. 37 | """ 38 | text = PowerShellActivator.quote(s) 39 | return text.replace('"', '""') if sys.platform == "win32" else text 40 | 41 | def activate_call(self, script): 42 | # Commands are called without quotes in PowerShell 43 | cmd = self.activate_cmd 44 | scr = self.quote(str(script)) 45 | return f"{cmd} {scr}".strip() 46 | 47 | activation_tester(PowerShell) 48 | -------------------------------------------------------------------------------- /tests/unit/activation/test_python_activator.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import sys 5 | from ast import literal_eval 6 | from textwrap import dedent 7 | 8 | from virtualenv.activation import PythonActivator 9 | from virtualenv.info import IS_WIN 10 | 11 | 12 | def test_python(raise_on_non_source_class, activation_tester): 13 | class Python(raise_on_non_source_class): 14 | def __init__(self, session) -> None: 15 | super().__init__( 16 | PythonActivator, 17 | session, 18 | sys.executable, 19 | activate_script="activate_this.py", 20 | extension="py", 21 | non_source_fail_message="You must use import runpy; runpy.run_path(this_file)", 22 | ) 23 | self.unix_line_ending = not IS_WIN 24 | 25 | def env(self, tmp_path): 26 | env = os.environ.copy() 27 | env["PYTHONIOENCODING"] = "utf-8" 28 | for key in ("VIRTUAL_ENV", "PYTHONPATH"): 29 | env.pop(str(key), None) 30 | env["PATH"] = os.pathsep.join([str(tmp_path), str(tmp_path / "other")]) 31 | return env 32 | 33 | @staticmethod 34 | def _get_test_lines(activate_script): 35 | raw = f""" 36 | import os 37 | import sys 38 | import platform 39 | import runpy 40 | 41 | def print_r(value): 42 | print(repr(value)) 43 | 44 | print_r(os.environ.get("VIRTUAL_ENV")) 45 | print_r(os.environ.get("VIRTUAL_ENV_PROMPT")) 46 | print_r(os.environ.get("PATH").split(os.pathsep)) 47 | print_r(sys.path) 48 | 49 | file_at = {str(activate_script)!r} 50 | # CPython 2 requires non-ascii path open to be unicode 51 | runpy.run_path(file_at) 52 | print_r(os.environ.get("VIRTUAL_ENV")) 53 | print_r(os.environ.get("VIRTUAL_ENV_PROMPT")) 54 | print_r(os.environ.get("PATH").split(os.pathsep)) 55 | print_r(sys.path) 56 | 57 | import pydoc_test 58 | print_r(pydoc_test.__file__) 59 | """ 60 | return dedent(raw).splitlines() 61 | 62 | def assert_output(self, out, raw, tmp_path): # noqa: ARG002 63 | out = [literal_eval(i) for i in out] 64 | assert out[0] is None # start with VIRTUAL_ENV None 65 | assert out[1] is None # likewise for VIRTUAL_ENV_PROMPT 66 | 67 | prev_path = out[2] 68 | prev_sys_path = out[3] 69 | assert out[4] == str(self._creator.dest) # VIRTUAL_ENV now points to the virtual env folder 70 | 71 | assert out[5] == str(self._creator.env_name) # VIRTUAL_ENV_PROMPT now has the env name 72 | 73 | new_path = out[6] # PATH now starts with bin path of current 74 | assert ([str(self._creator.bin_dir), *prev_path]) == new_path 75 | 76 | # sys path contains the site package at its start 77 | new_sys_path = out[7] 78 | 79 | new_lib_paths = {str(i) for i in self._creator.libs} 80 | assert prev_sys_path == new_sys_path[len(new_lib_paths) :] 81 | assert new_lib_paths == set(new_sys_path[: len(new_lib_paths)]) 82 | 83 | # manage to import from activate site package 84 | dest = self.norm_path(self._creator.purelib / "pydoc_test.py") 85 | found = self.norm_path(out[8]) 86 | assert found.startswith(dest) 87 | 88 | def non_source_activate(self, activate_script): 89 | act = str(activate_script) 90 | return [*self._invoke_script, "-c", f"exec(open({act!r}).read())"] 91 | 92 | activation_tester(Python) 93 | -------------------------------------------------------------------------------- /tests/unit/config/cli/test_parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from contextlib import contextmanager 5 | 6 | import pytest 7 | 8 | from virtualenv.config.cli.parser import VirtualEnvConfigParser, VirtualEnvOptions 9 | from virtualenv.config.ini import IniConfig 10 | from virtualenv.run import session_via_cli 11 | 12 | 13 | @pytest.fixture 14 | def gen_parser_no_conf_env(monkeypatch, tmp_path): 15 | keys_to_delete = {key for key in os.environ if key.startswith("VIRTUALENV_")} 16 | for key in keys_to_delete: 17 | monkeypatch.delenv(key) 18 | monkeypatch.setenv(IniConfig.VIRTUALENV_CONFIG_FILE_ENV_VAR, str(tmp_path / "missing")) 19 | 20 | @contextmanager 21 | def _build(): 22 | parser = VirtualEnvConfigParser() 23 | 24 | def _run(*args): 25 | return parser.parse_args(args=args) 26 | 27 | yield parser, _run 28 | parser.enable_help() 29 | 30 | return _build 31 | 32 | 33 | def test_flag(gen_parser_no_conf_env): 34 | with gen_parser_no_conf_env() as (parser, run): 35 | parser.add_argument("--clear", dest="clear", action="store_true", help="it", default=False) 36 | result = run() 37 | assert result.clear is False 38 | result = run("--clear") 39 | assert result.clear is True 40 | 41 | 42 | def test_reset_app_data_does_not_conflict_clear(): 43 | options = VirtualEnvOptions() 44 | session_via_cli(["--clear", "venv"], options=options) 45 | assert options.clear is True 46 | assert options.reset_app_data is False 47 | 48 | 49 | def test_builtin_discovery_class_preferred(mocker): 50 | mocker.patch( 51 | "virtualenv.run.plugin.discovery._get_default_discovery", 52 | return_value=["pluginA", "pluginX", "builtin", "Aplugin", "Xplugin"], 53 | ) 54 | 55 | options = VirtualEnvOptions() 56 | session_via_cli(["venv"], options=options) 57 | assert options.discovery == "builtin" 58 | -------------------------------------------------------------------------------- /tests/unit/config/test_ini.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from textwrap import dedent 5 | 6 | import pytest 7 | 8 | from virtualenv.info import IS_PYPY, IS_WIN, fs_supports_symlink 9 | from virtualenv.run import session_via_cli 10 | 11 | 12 | @pytest.mark.skipif(not fs_supports_symlink(), reason="symlink is not supported") 13 | @pytest.mark.xfail( 14 | # https://doc.pypy.org/en/latest/install.html?highlight=symlink#download-a-pre-built-pypy 15 | IS_PYPY and IS_WIN and sys.version_info[0:2] >= (3, 9), 16 | reason="symlink is not supported", 17 | ) 18 | def test_ini_can_be_overwritten_by_flag(tmp_path, monkeypatch): 19 | custom_ini = tmp_path / "conf.ini" 20 | custom_ini.write_text( 21 | dedent( 22 | """ 23 | [virtualenv] 24 | copies = True 25 | """, 26 | ), 27 | encoding="utf-8", 28 | ) 29 | monkeypatch.setenv("VIRTUALENV_CONFIG_FILE", str(custom_ini)) 30 | 31 | result = session_via_cli(["venv", "--symlinks"]) 32 | 33 | symlinks = result.creator.symlinks 34 | assert symlinks is True 35 | -------------------------------------------------------------------------------- /tests/unit/create/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | It's possible to use multiple types of host pythons to create virtual environments and all should work: 3 | 4 | - host installation 5 | - invoking from a venv (if Python 3.3+) 6 | - invoking from an old style virtualenv (<17.0.0) 7 | - invoking from our own venv 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | import sys 13 | from subprocess import Popen 14 | 15 | import pytest 16 | 17 | from virtualenv.discovery.py_info import PythonInfo 18 | 19 | CURRENT = PythonInfo.current_system() 20 | 21 | 22 | def root(tmp_path_factory, session_app_data): # noqa: ARG001 23 | return CURRENT.system_executable 24 | 25 | 26 | def venv(tmp_path_factory, session_app_data): 27 | if CURRENT.is_venv: 28 | return sys.executable 29 | root_python = root(tmp_path_factory, session_app_data) 30 | dest = tmp_path_factory.mktemp("venv") 31 | process = Popen([str(root_python), "-m", "venv", "--without-pip", str(dest)]) 32 | process.communicate() 33 | # sadly creating a virtual environment does not tell us where the executable lives in general case 34 | # so discover using some heuristic 35 | return CURRENT.discover_exe(prefix=str(dest)).original_executable 36 | 37 | 38 | PYTHON = { 39 | "root": root, 40 | "venv": venv, 41 | } 42 | 43 | 44 | @pytest.fixture(params=list(PYTHON.values()), ids=list(PYTHON.keys()), scope="session") 45 | def python(request, tmp_path_factory, session_app_data): 46 | result = request.param(tmp_path_factory, session_app_data) 47 | if isinstance(result, Exception): 48 | pytest.skip(f"could not resolve interpreter based on {request.param.__name__} because {result}") 49 | if result is None: 50 | pytest.skip(f"requires interpreter with {request.param.__name__}") 51 | return result 52 | -------------------------------------------------------------------------------- /tests/unit/create/console_app/demo/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | def run(): 5 | print("magic") # noqa: T201 6 | 7 | 8 | if __name__ == "__main__": 9 | run() 10 | -------------------------------------------------------------------------------- /tests/unit/create/console_app/demo/__main__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | def run(): 5 | print("magic") # noqa: T201 6 | 7 | 8 | if __name__ == "__main__": 9 | run() 10 | -------------------------------------------------------------------------------- /tests/unit/create/console_app/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = demo 3 | version = 1.0.0 4 | description = magic package 5 | 6 | [options] 7 | packages = find: 8 | 9 | [options.entry_points] 10 | console_scripts = 11 | magic=demo.__main__:run 12 | 13 | [bdist_wheel] 14 | universal = true 15 | -------------------------------------------------------------------------------- /tests/unit/create/console_app/setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from setuptools import setup 4 | 5 | setup() 6 | -------------------------------------------------------------------------------- /tests/unit/create/test_interpreters.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from uuid import uuid4 5 | 6 | import pytest 7 | 8 | from virtualenv.discovery.py_info import PythonInfo 9 | from virtualenv.run import cli_run 10 | 11 | 12 | @pytest.mark.slow 13 | def test_failed_to_find_bad_spec(): 14 | of_id = uuid4().hex 15 | with pytest.raises(RuntimeError) as context: 16 | cli_run(["-p", of_id]) 17 | msg = repr(RuntimeError(f"failed to find interpreter for Builtin discover of python_spec={of_id!r}")) 18 | assert repr(context.value) == msg 19 | 20 | 21 | SYSTEM = PythonInfo.current_system() 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "of_id", 26 | ({sys.executable} if sys.executable != SYSTEM.executable else set()) | {SYSTEM.implementation}, 27 | ) 28 | def test_failed_to_find_implementation(of_id, mocker): 29 | mocker.patch("virtualenv.run.plugin.creators.CreatorSelector._OPTIONS", return_value={}) 30 | with pytest.raises(RuntimeError) as context: 31 | cli_run(["-p", of_id]) 32 | assert repr(context.value) == repr(RuntimeError(f"No virtualenv implementation for {PythonInfo.current_system()}")) 33 | -------------------------------------------------------------------------------- /tests/unit/create/via_global_ref/builtin/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | import pytest 7 | from testing import path 8 | from testing.py_info import read_fixture 9 | 10 | # Allows to import from `testing` into test submodules. 11 | sys.path.append(str(Path(__file__).parent)) 12 | 13 | 14 | @pytest.fixture 15 | def py_info(py_info_name): 16 | return read_fixture(py_info_name) 17 | 18 | 19 | @pytest.fixture 20 | def mock_files(mocker): 21 | return lambda paths, files: path.mock_files(mocker, paths, files) 22 | 23 | 24 | @pytest.fixture 25 | def mock_pypy_libs(mocker): 26 | return lambda pypy, libs: path.mock_pypy_libs(mocker, pypy, libs) 27 | -------------------------------------------------------------------------------- /tests/unit/create/via_global_ref/builtin/cpython/cpython3_win_embed.json: -------------------------------------------------------------------------------- 1 | { 2 | "platform": "win32", 3 | "implementation": "CPython", 4 | "version_info": { 5 | "major": 3, 6 | "minor": 10, 7 | "micro": 4, 8 | "releaselevel": "final", 9 | "serial": 0 10 | }, 11 | "architecture": 64, 12 | "version_nodot": "310", 13 | "version": "3.10.4 (tags/v3.10.4:9d38120, Mar 23 2022, 23:13:41) [MSC v.1929 64 bit (AMD64)]", 14 | "os": "nt", 15 | "prefix": "c:\\path\\to\\python", 16 | "base_prefix": "c:\\path\\to\\python", 17 | "real_prefix": null, 18 | "base_exec_prefix": "c:\\path\\to\\python", 19 | "exec_prefix": "c:\\path\\to\\python", 20 | "executable": "c:\\path\\to\\python\\python.exe", 21 | "original_executable": "c:\\path\\to\\python\\python.exe", 22 | "system_executable": "c:\\path\\to\\python\\python.exe", 23 | "has_venv": false, 24 | "path": [ 25 | "c:\\path\\to\\python\\Scripts\\virtualenv.exe", 26 | "c:\\path\\to\\python\\python310.zip", 27 | "c:\\path\\to\\python", 28 | "c:\\path\\to\\python\\Lib\\site-packages" 29 | ], 30 | "file_system_encoding": "utf-8", 31 | "stdout_encoding": "utf-8", 32 | "sysconfig_scheme": null, 33 | "sysconfig_paths": { 34 | "stdlib": "{installed_base}/Lib", 35 | "platstdlib": "{base}/Lib", 36 | "purelib": "{base}/Lib/site-packages", 37 | "platlib": "{base}/Lib/site-packages", 38 | "include": "{installed_base}/Include", 39 | "scripts": "{base}/Scripts", 40 | "data": "{base}" 41 | }, 42 | "distutils_install": { 43 | "purelib": "Lib\\site-packages", 44 | "platlib": "Lib\\site-packages", 45 | "headers": "Include\\UNKNOWN", 46 | "scripts": "Scripts", 47 | "data": "" 48 | }, 49 | "sysconfig": { 50 | "makefile_filename": "c:\\path\\to\\python\\Lib\\config\\Makefile" 51 | }, 52 | "sysconfig_vars": { 53 | "PYTHONFRAMEWORK": "", 54 | "installed_base": "c:\\path\\to\\python", 55 | "base": "c:\\path\\to\\python" 56 | }, 57 | "system_stdlib": "c:\\path\\to\\python\\Lib", 58 | "system_stdlib_platform": "c:\\path\\to\\python\\Lib", 59 | "max_size": 9223372036854775807, 60 | "_creators": null, 61 | "free_threaded": false 62 | } 63 | -------------------------------------------------------------------------------- /tests/unit/create/via_global_ref/builtin/pypy/deb_pypy37.json: -------------------------------------------------------------------------------- 1 | { 2 | "platform": "linux", 3 | "implementation": "PyPy", 4 | "pypy_version_info": [7, 3, 7, "final", 0], 5 | "version_info": { 6 | "major": 3, 7 | "minor": 7, 8 | "micro": 12, 9 | "releaselevel": "final", 10 | "serial": 0 11 | }, 12 | "architecture": 64, 13 | "version": "3.7.12 (7.3.7+dfsg-5, Jan 27 2022, 12:27:44)\\n[PyPy 7.3.7 with GCC 11.2.0]", 14 | "os": "posix", 15 | "prefix": "/usr/lib/pypy3", 16 | "base_prefix": "/usr/lib/pypy3", 17 | "real_prefix": null, 18 | "base_exec_prefix": "/usr/lib/pypy3", 19 | "exec_prefix": "/usr/lib/pypy3", 20 | "executable": "/usr/bin/pypy3", 21 | "original_executable": "/usr/bin/pypy3", 22 | "system_executable": "/usr/bin/pypy3", 23 | "has_venv": true, 24 | "path": [ 25 | "/usr/lib/pypy3/lib_pypy/__extensions__", 26 | "/usr/lib/pypy3/lib_pypy", 27 | "/usr/lib/pypy3/lib-python/3", 28 | "/usr/lib/pypy3/lib-python/3/lib-tk", 29 | "/usr/lib/pypy3/lib-python/3/plat-linux2", 30 | "/usr/local/lib/pypy3.7/dist-packages", 31 | "/usr/lib/python3/dist-packages" 32 | ], 33 | "file_system_encoding": "utf-8", 34 | "stdout_encoding": "UTF-8", 35 | "sysconfig_scheme": null, 36 | "sysconfig_paths": { 37 | "stdlib": "{base}/lib-python/{py_version_short}", 38 | "platstdlib": "{base}/lib-python/{py_version_short}", 39 | "purelib": "{base}/../../local/lib/pypy{py_version_short}/lib-python", 40 | "platlib": "{base}/../../local/lib/pypy{py_version_short}/lib-python", 41 | "include": "{base}/include", 42 | "scripts": "{base}/../../local/bin", 43 | "data": "{base}/../../local" 44 | }, 45 | "distutils_install": { 46 | "purelib": "site-packages", 47 | "platlib": "site-packages", 48 | "headers": "include/UNKNOWN", 49 | "scripts": "bin", 50 | "data": "" 51 | }, 52 | "sysconfig": { 53 | "makefile_filename": "/usr/lib/pypy3/lib-python/3.7/config-3.7-x86_64-linux-gnu/Makefile" 54 | }, 55 | "sysconfig_vars": { 56 | "base": "/usr/lib/pypy3", 57 | "py_version_short": "3.7", 58 | "PYTHONFRAMEWORK": "" 59 | }, 60 | "system_stdlib": "/usr/lib/pypy3/lib-python/3.7", 61 | "system_stdlib_platform": "/usr/lib/pypy3/lib-python/3.7", 62 | "max_size": 9223372036854775807, 63 | "_creators": null, 64 | "free_threaded": false 65 | } 66 | -------------------------------------------------------------------------------- /tests/unit/create/via_global_ref/builtin/pypy/deb_pypy38.json: -------------------------------------------------------------------------------- 1 | { 2 | "platform": "linux", 3 | "implementation": "PyPy", 4 | "pypy_version_info": [7, 3, 8, "final", 0], 5 | "version_info": { 6 | "major": 3, 7 | "minor": 8, 8 | "micro": 12, 9 | "releaselevel": "final", 10 | "serial": 0 11 | }, 12 | "architecture": 64, 13 | "version": "3.8.12 (7.3.8+dfsg-2, Mar 05 2022, 02:04:42)\\n[PyPy 7.3.8 with GCC 11.2.0]", 14 | "os": "posix", 15 | "prefix": "/usr", 16 | "base_prefix": "/usr", 17 | "real_prefix": null, 18 | "base_exec_prefix": "/usr", 19 | "exec_prefix": "/usr", 20 | "executable": "/usr/bin/pypy3", 21 | "original_executable": "/usr/bin/pypy3", 22 | "system_executable": "/usr/bin/pypy3", 23 | "has_venv": true, 24 | "path": [ 25 | "/usr/lib/pypy3.8", 26 | "/usr/local/lib/pypy3.8/dist-packages", 27 | "/usr/lib/python3/dist-packages" 28 | ], 29 | "file_system_encoding": "utf-8", 30 | "stdout_encoding": "UTF-8", 31 | "sysconfig_scheme": null, 32 | "sysconfig_paths": { 33 | "stdlib": "{installed_base}/lib/{implementation_lower}{py_version_short}", 34 | "platstdlib": "{platbase}/lib/{implementation_lower}{py_version_short}", 35 | "purelib": "{base}/local/lib/{implementation_lower}{py_version_short}/dist-packages", 36 | "platlib": "{platbase}/local/lib/{implementation_lower}{py_version_short}/dist-packages", 37 | "include": "{installed_base}/local/include/{implementation_lower}{py_version_short}{abiflags}", 38 | "scripts": "{base}/local/bin", 39 | "data": "{base}" 40 | }, 41 | "distutils_install": { 42 | "purelib": "lib/pypy3.8/site-packages", 43 | "platlib": "lib/pypy3.8/site-packages", 44 | "headers": "include/pypy3.8/UNKNOWN", 45 | "scripts": "bin", 46 | "data": "" 47 | }, 48 | "sysconfig": { 49 | "makefile_filename": "/usr/lib/pypy3.8/config-3.8-x86_64-linux-gnu/Makefile" 50 | }, 51 | "sysconfig_vars": { 52 | "installed_base": "/usr", 53 | "implementation_lower": "pypy", 54 | "py_version_short": "3.8", 55 | "platbase": "/usr", 56 | "base": "/usr", 57 | "abiflags": "", 58 | "PYTHONFRAMEWORK": "" 59 | }, 60 | "system_stdlib": "/usr/lib/pypy3.8", 61 | "system_stdlib_platform": "/usr/lib/pypy3.8", 62 | "max_size": 9223372036854775807, 63 | "_creators": null, 64 | "free_threaded": false 65 | } 66 | -------------------------------------------------------------------------------- /tests/unit/create/via_global_ref/builtin/pypy/portable_pypy38.json: -------------------------------------------------------------------------------- 1 | { 2 | "platform": "linux", 3 | "implementation": "PyPy", 4 | "pypy_version_info": [7, 3, 8, "final", 0], 5 | "version_info": { 6 | "major": 3, 7 | "minor": 8, 8 | "micro": 12, 9 | "releaselevel": "final", 10 | "serial": 0 11 | }, 12 | "architecture": 64, 13 | "version": "3.8.12 (d00b0afd2a5dd3c13fcda75d738262c864c62fa7, Feb 18 2022, 09:52:33)\\n[PyPy 7.3.8 with GCC 10.2.1 20210130 (Red Hat 10.2.1-11)]", 14 | "os": "posix", 15 | "prefix": "/tmp/pypy3.8-v7.3.8-linux64", 16 | "base_prefix": "/tmp/pypy3.8-v7.3.8-linux64", 17 | "real_prefix": null, 18 | "base_exec_prefix": "/tmp/pypy3.8-v7.3.8-linux64", 19 | "exec_prefix": "/tmp/pypy3.8-v7.3.8-linux64", 20 | "executable": "/tmp/pypy3.8-v7.3.8-linux64/bin/pypy", 21 | "original_executable": "/tmp/pypy3.8-v7.3.8-linux64/bin/pypy", 22 | "system_executable": "/tmp/pypy3.8-v7.3.8-linux64/bin/pypy", 23 | "has_venv": true, 24 | "path": [ 25 | "/tmp/pypy3.8-v7.3.8-linux64/lib/pypy3.8", 26 | "/tmp/pypy3.8-v7.3.8-linux64/lib/pypy3.8/site-packages" 27 | ], 28 | "file_system_encoding": "utf-8", 29 | "stdout_encoding": "UTF-8", 30 | "sysconfig_scheme": null, 31 | "sysconfig_paths": { 32 | "stdlib": "{installed_base}/lib/{implementation_lower}{py_version_short}", 33 | "platstdlib": "{platbase}/lib/{implementation_lower}{py_version_short}", 34 | "purelib": "{base}/lib/{implementation_lower}{py_version_short}/site-packages", 35 | "platlib": "{platbase}/lib/{implementation_lower}{py_version_short}/site-packages", 36 | "include": "{installed_base}/include/{implementation_lower}{py_version_short}{abiflags}", 37 | "scripts": "{base}/bin", 38 | "data": "{base}" 39 | }, 40 | "distutils_install": { 41 | "purelib": "lib/pypy3.8/site-packages", 42 | "platlib": "lib/pypy3.8/site-packages", 43 | "headers": "include/pypy3.8/UNKNOWN", 44 | "scripts": "bin", 45 | "data": "" 46 | }, 47 | "sysconfig": { 48 | "makefile_filename": "/tmp/pypy3.8-v7.3.8-linux64/lib/pypy3.8/config-3.8-x86_64-linux-gnu/Makefile" 49 | }, 50 | "sysconfig_vars": { 51 | "installed_base": "/tmp/pypy3.8-v7.3.8-linux64", 52 | "implementation_lower": "pypy", 53 | "py_version_short": "3.8", 54 | "platbase": "/tmp/pypy3.8-v7.3.8-linux64", 55 | "base": "/tmp/pypy3.8-v7.3.8-linux64", 56 | "abiflags": "", 57 | "PYTHONFRAMEWORK": "" 58 | }, 59 | "system_stdlib": "/tmp/pypy3.8-v7.3.8-linux64/lib/pypy3.8", 60 | "system_stdlib_platform": "/tmp/pypy3.8-v7.3.8-linux64/lib/pypy3.8", 61 | "max_size": 9223372036854775807, 62 | "_creators": null, 63 | "free_threaded": false 64 | } 65 | -------------------------------------------------------------------------------- /tests/unit/create/via_global_ref/builtin/pypy/test_pypy3.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from testing.helpers import contains_exe, contains_ref 5 | from testing.path import join as path 6 | 7 | from virtualenv.create.via_global_ref.builtin.pypy.pypy3 import PyPy3Posix 8 | 9 | PYPY3_PATH = ( 10 | "virtualenv.create.via_global_ref.builtin.pypy.common.Path", 11 | "virtualenv.create.via_global_ref.builtin.pypy.pypy3.Path", 12 | ) 13 | 14 | 15 | # In `PyPy3Posix.sources()` `host_lib` will be broken in Python 2 for Windows, 16 | # so `py_file` will not be in sources. 17 | @pytest.mark.parametrize("py_info_name", ["portable_pypy38"]) 18 | def test_portable_pypy3_virtualenvs_get_their_libs(py_info, mock_files, mock_pypy_libs): 19 | py_file = path(py_info.prefix, "lib/libgdbm.so.4") 20 | mock_files(PYPY3_PATH, [py_info.system_executable, py_file]) 21 | lib_file = path(py_info.prefix, "bin/libpypy3-c.so") 22 | mock_pypy_libs(PyPy3Posix, [lib_file]) 23 | sources = tuple(PyPy3Posix.sources(interpreter=py_info)) 24 | assert len(sources) > 2 25 | assert contains_exe(sources, py_info.system_executable) 26 | assert contains_ref(sources, py_file) 27 | assert contains_ref(sources, lib_file) 28 | 29 | 30 | @pytest.mark.parametrize("py_info_name", ["deb_pypy37"]) 31 | def test_debian_pypy37_virtualenvs(py_info, mock_files, mock_pypy_libs): 32 | # Debian's pypy3 layout, installed to /usr, before 3.8 allowed a /usr prefix 33 | mock_files(PYPY3_PATH, [py_info.system_executable]) 34 | lib_file = path(py_info.prefix, "bin/libpypy3-c.so") 35 | mock_pypy_libs(PyPy3Posix, [lib_file]) 36 | sources = tuple(PyPy3Posix.sources(interpreter=py_info)) 37 | assert len(sources) == 2 38 | assert contains_exe(sources, py_info.system_executable) 39 | assert contains_ref(sources, lib_file) 40 | 41 | 42 | @pytest.mark.parametrize("py_info_name", ["deb_pypy38"]) 43 | def test_debian_pypy38_virtualenvs_exclude_usr(py_info, mock_files, mock_pypy_libs): 44 | mock_files(PYPY3_PATH, [py_info.system_executable, "/usr/lib/foo"]) 45 | # libpypy3-c.so lives on the ld search path 46 | mock_pypy_libs(PyPy3Posix, []) 47 | sources = tuple(PyPy3Posix.sources(interpreter=py_info)) 48 | assert len(sources) == 1 49 | assert contains_exe(sources, py_info.system_executable) 50 | -------------------------------------------------------------------------------- /tests/unit/create/via_global_ref/builtin/testing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/virtualenv/61977075c28fee71f134bf8398b3e516b8b8030a/tests/unit/create/via_global_ref/builtin/testing/__init__.py -------------------------------------------------------------------------------- /tests/unit/create/via_global_ref/builtin/testing/helpers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from functools import reduce 4 | from pathlib import Path 5 | 6 | from virtualenv.create.via_global_ref.builtin.ref import ExePathRefToDest, PathRef 7 | 8 | 9 | def is_ref(source): 10 | return isinstance(source, PathRef) 11 | 12 | 13 | def is_exe(source): 14 | return type(source) is ExePathRefToDest 15 | 16 | 17 | def has_src(src): 18 | return lambda ref: ref.src.as_posix() == Path(src).as_posix() 19 | 20 | 21 | def has_target(target): 22 | return lambda ref: ref.base == target 23 | 24 | 25 | def apply_filter(values, function): 26 | return filter(function, values) 27 | 28 | 29 | def filterby(filters, sources): 30 | return reduce(apply_filter, filters, sources) 31 | 32 | 33 | def contains_exe(sources, src, target=None): 34 | filters = is_exe, has_src(src), target and has_target(target) 35 | return any(filterby(filters, sources)) 36 | 37 | 38 | def contains_ref(sources, src): 39 | filters = is_ref, has_src(src) 40 | return any(filterby(filters, sources)) 41 | -------------------------------------------------------------------------------- /tests/unit/create/via_global_ref/builtin/testing/path.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from itertools import chain 5 | from operator import attrgetter as attr 6 | from pathlib import Path 7 | 8 | 9 | def is_name(path): 10 | return str(path) == path.name 11 | 12 | 13 | class FakeDataABC(ABC): 14 | """Provides data to mock the `Path`""" 15 | 16 | @property 17 | @abstractmethod 18 | def filelist(self): 19 | """To mock a dir, just mock any child file.""" 20 | msg = "Collection of (str) file paths to mock" 21 | raise NotImplementedError(msg) 22 | 23 | @property 24 | def fake_files(self): 25 | return map(type(self), self.filelist) 26 | 27 | @property 28 | def fake_dirs(self): 29 | return set(chain(*map(attr("parents"), self.fake_files))) 30 | 31 | @property 32 | def contained_fake_names(self): 33 | return filter(is_name, self.fake_content) 34 | 35 | @property 36 | def fake_content(self): 37 | return filter(None, map(self.fake_child, self.fake_files)) 38 | 39 | def fake_child(self, path): 40 | try: 41 | return path.relative_to(self) 42 | except ValueError: 43 | return None 44 | 45 | 46 | class PathMockABC(FakeDataABC, Path): 47 | """Mocks the behavior of `Path`""" 48 | 49 | _flavour = getattr(Path(), "_flavour", None) 50 | if hasattr(_flavour, "altsep"): 51 | # Allows to pass some tests for Windows via PosixPath. 52 | _flavour.altsep = _flavour.altsep or "\\" 53 | 54 | # Python 3.13 renamed _flavour to parser 55 | parser = getattr(Path(), "parser", None) 56 | if hasattr(parser, "altsep"): 57 | parser.altsep = parser.altsep or "\\" 58 | 59 | def exists(self): 60 | return self.is_file() or self.is_dir() 61 | 62 | def is_file(self): 63 | return self in self.fake_files 64 | 65 | def is_dir(self): 66 | return self in self.fake_dirs 67 | 68 | def resolve(self): 69 | return self 70 | 71 | def iterdir(self): 72 | if not self.is_dir(): 73 | msg = f"No such mocked dir: '{self}'" 74 | raise FileNotFoundError(msg) 75 | yield from map(self.joinpath, self.contained_fake_names) 76 | 77 | 78 | def MetaPathMock(filelist): # noqa: N802 79 | """ 80 | Metaclass that creates a `PathMock` class with the `filelist` defined. 81 | """ 82 | return type("PathMock", (PathMockABC,), {"filelist": filelist}) 83 | 84 | 85 | def mock_files(mocker, pathlist, filelist): 86 | PathMock = MetaPathMock(set(filelist)) # noqa: N806 87 | for path in pathlist: 88 | mocker.patch(path, PathMock) 89 | 90 | 91 | def mock_pypy_libs(mocker, pypy_creator_cls, libs): 92 | paths = tuple(set(map(Path, libs))) 93 | mocker.patch.object(pypy_creator_cls, "_shared_libs", return_value=paths) 94 | 95 | 96 | def join(*chunks): 97 | line = "".join(chunks) 98 | sep = ("\\" in line and "\\") or ("/" in line and "/") or "/" 99 | return sep.join(chunks) 100 | -------------------------------------------------------------------------------- /tests/unit/create/via_global_ref/builtin/testing/py_info.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from virtualenv.discovery.py_info import PythonInfo 6 | 7 | 8 | def fixture_file(fixture_name): 9 | file_mask = f"*{fixture_name}.json" 10 | files = Path(__file__).parent.parent.rglob(file_mask) 11 | try: 12 | return next(files) 13 | except StopIteration as exc: 14 | # Fixture file was not found in the testing root and its subdirs. 15 | error = FileNotFoundError 16 | raise error(file_mask) from exc 17 | 18 | 19 | def read_fixture(fixture_name): 20 | fixture_json = fixture_file(fixture_name).read_text(encoding="utf-8") 21 | return PythonInfo._from_json(fixture_json) # noqa: SLF001 22 | -------------------------------------------------------------------------------- /tests/unit/create/via_global_ref/greet/greet2.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | static PyObject * greet(PyObject * self, PyObject * args) { 5 | const char * name; 6 | if (!PyArg_ParseTuple(args, "s", & name)) { 7 | return NULL; 8 | } 9 | printf("Hello %s!\n", name); 10 | Py_RETURN_NONE; 11 | } 12 | 13 | static PyMethodDef GreetMethods[] = { 14 | { 15 | "greet", 16 | greet, 17 | METH_VARARGS, 18 | "Greet an entity." 19 | }, 20 | { 21 | NULL, 22 | NULL, 23 | 0, 24 | NULL 25 | } 26 | }; 27 | 28 | PyMODINIT_FUNC initgreet(void) { 29 | (void) Py_InitModule("greet", GreetMethods); 30 | } 31 | -------------------------------------------------------------------------------- /tests/unit/create/via_global_ref/greet/greet3.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | static PyObject * greet(PyObject * self, PyObject * args) { 5 | const char * name; 6 | if (!PyArg_ParseTuple(args, "s", & name)) { 7 | return NULL; 8 | } 9 | printf("Hello %s!\n", name); 10 | Py_RETURN_NONE; 11 | } 12 | 13 | static PyMethodDef GreetMethods[] = { 14 | { 15 | "greet", 16 | greet, 17 | METH_VARARGS, 18 | "Greet an entity." 19 | }, 20 | { 21 | NULL, 22 | NULL, 23 | 0, 24 | NULL 25 | } 26 | }; 27 | 28 | static struct PyModuleDef greet_definition = { 29 | PyModuleDef_HEAD_INIT, 30 | "greet", 31 | "A Python module that prints 'greet world' from C code.", 32 | -1, 33 | GreetMethods 34 | }; 35 | 36 | PyMODINIT_FUNC PyInit_greet(void) { 37 | return PyModule_Create( & greet_definition); 38 | } 39 | -------------------------------------------------------------------------------- /tests/unit/create/via_global_ref/greet/setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | 5 | from setuptools import Extension, setup 6 | 7 | setup( 8 | name="greet", # package name 9 | version="1.0", # package version 10 | ext_modules=[ 11 | Extension( 12 | "greet", 13 | [f"greet{sys.version_info[0]}.c"], # extension to package 14 | ), # C code to compile to run as extension 15 | ], 16 | ) 17 | -------------------------------------------------------------------------------- /tests/unit/create/via_global_ref/test_api.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from virtualenv.create.via_global_ref import api 4 | 5 | 6 | def test_can_symlink_when_symlinks_not_enabled(mocker): 7 | mocker.patch.object(api, "fs_supports_symlink", return_value=False) 8 | assert api.ViaGlobalRefMeta().can_symlink is False 9 | -------------------------------------------------------------------------------- /tests/unit/create/via_global_ref/test_build_c_ext.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import shutil 5 | import subprocess 6 | from pathlib import Path 7 | from subprocess import Popen 8 | 9 | import pytest 10 | 11 | from virtualenv.discovery.py_info import PythonInfo 12 | from virtualenv.run import cli_run 13 | 14 | CURRENT = PythonInfo.current_system() 15 | CREATOR_CLASSES = CURRENT.creators().key_to_class 16 | 17 | 18 | def builtin_shows_marker_missing(): 19 | builtin_classs = CREATOR_CLASSES.get("builtin") 20 | if builtin_classs is None: 21 | return False 22 | host_include_marker = getattr(builtin_classs, "host_include_marker", None) 23 | if host_include_marker is None: 24 | return False 25 | marker = host_include_marker(CURRENT) 26 | return not marker.exists() 27 | 28 | 29 | @pytest.mark.slow 30 | @pytest.mark.xfail( 31 | condition=bool(os.environ.get("CI_RUN")), 32 | strict=False, 33 | reason="did not manage to setup CI to run with VC 14.1 C++ compiler, but passes locally", 34 | ) 35 | @pytest.mark.skipif( 36 | not Path(CURRENT.system_include).exists() and not builtin_shows_marker_missing(), 37 | reason="Building C-Extensions requires header files with host python", 38 | ) 39 | @pytest.mark.parametrize("creator", [i for i in CREATOR_CLASSES if i != "builtin"]) 40 | def test_can_build_c_extensions(creator, tmp_path, coverage_env): 41 | env, greet = tmp_path / "env", str(tmp_path / "greet") 42 | shutil.copytree(str(Path(__file__).parent.resolve() / "greet"), greet) 43 | session = cli_run(["--creator", creator, "--seeder", "app-data", str(env), "-vvv"]) 44 | coverage_env() 45 | setuptools_index_args = () 46 | if CURRENT.version_info >= (3, 12): 47 | # requires to be able to install setuptools as build dependency 48 | setuptools_index_args = ( 49 | "--find-links", 50 | "https://pypi.org/simple/setuptools/", 51 | ) 52 | 53 | cmd = [ 54 | str(session.creator.script("pip")), 55 | "install", 56 | "--no-index", 57 | *setuptools_index_args, 58 | "--no-deps", 59 | "--disable-pip-version-check", 60 | "-vvv", 61 | greet, 62 | ] 63 | process = Popen(cmd) 64 | process.communicate() 65 | assert process.returncode == 0 66 | 67 | process = Popen( 68 | [str(session.creator.exe), "-c", "import greet; greet.greet('World')"], 69 | universal_newlines=True, 70 | stdout=subprocess.PIPE, 71 | encoding="utf-8", 72 | ) 73 | out, _ = process.communicate() 74 | assert process.returncode == 0 75 | assert out == "Hello World!\n" 76 | -------------------------------------------------------------------------------- /tests/unit/discovery/py_info/test_py_info_exe_based_of.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os 5 | from pathlib import Path 6 | 7 | import pytest 8 | 9 | from virtualenv.discovery.py_info import EXTENSIONS, PythonInfo 10 | from virtualenv.info import IS_WIN, fs_is_case_sensitive, fs_supports_symlink 11 | 12 | CURRENT = PythonInfo.current() 13 | 14 | 15 | def test_discover_empty_folder(tmp_path, session_app_data): 16 | with pytest.raises(RuntimeError): 17 | CURRENT.discover_exe(session_app_data, prefix=str(tmp_path)) 18 | 19 | 20 | BASE = (CURRENT.install_path("scripts"), ".") 21 | 22 | 23 | @pytest.mark.skipif(not fs_supports_symlink(), reason="symlink is not supported") 24 | @pytest.mark.parametrize("suffix", sorted({".exe", ".cmd", ""} & set(EXTENSIONS) if IS_WIN else [""])) 25 | @pytest.mark.parametrize("into", BASE) 26 | @pytest.mark.parametrize("arch", [CURRENT.architecture, ""]) 27 | @pytest.mark.parametrize("version", [".".join(str(i) for i in CURRENT.version_info[0:i]) for i in range(3, 0, -1)]) 28 | @pytest.mark.parametrize("impl", [CURRENT.implementation, "python"]) 29 | def test_discover_ok(tmp_path, suffix, impl, version, arch, into, caplog, session_app_data): # noqa: PLR0913 30 | caplog.set_level(logging.DEBUG) 31 | folder = tmp_path / into 32 | folder.mkdir(parents=True, exist_ok=True) 33 | name = f"{impl}{version}{'t' if CURRENT.free_threaded else ''}" 34 | if arch: 35 | name += f"-{arch}" 36 | name += suffix 37 | dest = folder / name 38 | os.symlink(CURRENT.executable, str(dest)) 39 | pyvenv = Path(CURRENT.executable).parents[1] / "pyvenv.cfg" 40 | if pyvenv.exists(): 41 | (folder / pyvenv.name).write_text(pyvenv.read_text(encoding="utf-8"), encoding="utf-8") 42 | inside_folder = str(tmp_path) 43 | base = CURRENT.discover_exe(session_app_data, inside_folder) 44 | found = base.executable 45 | dest_str = str(dest) 46 | if not fs_is_case_sensitive(): 47 | found = found.lower() 48 | dest_str = dest_str.lower() 49 | assert found == dest_str 50 | assert len(caplog.messages) >= 1, caplog.text 51 | assert "get interpreter info via cmd: " in caplog.text 52 | 53 | dest.rename(dest.parent / (dest.name + "-1")) 54 | CURRENT._cache_exe_discovery.clear() # noqa: SLF001 55 | with pytest.raises(RuntimeError): 56 | CURRENT.discover_exe(session_app_data, inside_folder) 57 | -------------------------------------------------------------------------------- /tests/unit/discovery/windows/test_windows.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | 5 | import pytest 6 | 7 | from virtualenv.discovery.py_spec import PythonSpec 8 | 9 | 10 | @pytest.mark.skipif(sys.platform != "win32", reason="no Windows registry") 11 | @pytest.mark.usefixtures("_mock_registry") 12 | @pytest.mark.usefixtures("_populate_pyinfo_cache") 13 | @pytest.mark.parametrize( 14 | ("string_spec", "expected_exe"), 15 | [ 16 | # 64-bit over 32-bit 17 | ("python3.10", "C:\\Users\\user\\Miniconda3-64\\python.exe"), 18 | ("cpython3.10", "C:\\Users\\user\\Miniconda3-64\\python.exe"), 19 | # 1 installation of 3.9 available 20 | ("python3.12", "C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"), 21 | ("cpython3.12", "C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"), 22 | # resolves to highest available version 23 | ("python", "C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python313\\python3.13t.exe"), 24 | ("cpython", "C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python313\\python3.13t.exe"), 25 | ("python3", "C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"), 26 | ("cpython3", "C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"), 27 | # Non-standard org name 28 | ("python3.6", "Z:\\CompanyA\\Python\\3.6\\python.exe"), 29 | ("cpython3.6", "Z:\\CompanyA\\Python\\3.6\\python.exe"), 30 | # free-threaded 31 | ("3t", "C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python313\\python3.13t.exe"), 32 | ("python3.13t", "C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python313\\python3.13t.exe"), 33 | ], 34 | ) 35 | def test_propose_interpreters(string_spec, expected_exe): 36 | from virtualenv.discovery.windows import propose_interpreters # noqa: PLC0415 37 | 38 | spec = PythonSpec.from_string_spec(string_spec) 39 | interpreter = next(propose_interpreters(spec=spec, cache_dir=None, env=None)) 40 | assert interpreter.executable == expected_exe 41 | -------------------------------------------------------------------------------- /tests/unit/seed/embed/test_base_embed.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from typing import TYPE_CHECKING 5 | 6 | import pytest 7 | 8 | from virtualenv.run import session_via_cli 9 | 10 | if TYPE_CHECKING: 11 | from pathlib import Path 12 | 13 | 14 | @pytest.mark.parametrize( 15 | ("args", "download"), 16 | [([], False), (["--no-download"], False), (["--never-download"], False), (["--download"], True)], 17 | ) 18 | def test_download_cli_flag(args, download, tmp_path): 19 | session = session_via_cli([*args, str(tmp_path)]) 20 | assert session.seeder.download is download 21 | 22 | 23 | @pytest.mark.skipif(sys.version_info[:2] == (3, 8), reason="We still bundle wheel for Python 3.8") 24 | @pytest.mark.parametrize("flag", ["--no-wheel", "--wheel=none", "--wheel=embed", "--wheel=bundle"]) 25 | def test_wheel_cli_flags_do_nothing(tmp_path, flag): 26 | session = session_via_cli([flag, str(tmp_path)]) 27 | if sys.version_info[:2] >= (3, 12): 28 | expected = {"pip": "bundle"} 29 | else: 30 | expected = {"pip": "bundle", "setuptools": "bundle"} 31 | assert session.seeder.distribution_to_versions() == expected 32 | 33 | 34 | @pytest.mark.skipif(sys.version_info[:2] == (3, 8), reason="We still bundle wheel for Python 3.8") 35 | @pytest.mark.parametrize("flag", ["--no-wheel", "--wheel=none", "--wheel=embed", "--wheel=bundle"]) 36 | def test_wheel_cli_flags_warn(tmp_path, flag, capsys): 37 | session_via_cli([flag, str(tmp_path)]) 38 | out, err = capsys.readouterr() 39 | assert "The --no-wheel and --wheel options are deprecated." in out + err 40 | 41 | 42 | @pytest.mark.skipif(sys.version_info[:2] == (3, 8), reason="We still bundle wheel for Python 3.8") 43 | def test_unused_wheel_cli_flags_dont_warn(tmp_path, capsys): 44 | session_via_cli([str(tmp_path)]) 45 | out, err = capsys.readouterr() 46 | assert "The --no-wheel and --wheel options are deprecated." not in out + err 47 | 48 | 49 | @pytest.mark.skipif(sys.version_info[:2] != (3, 8), reason="We only bundle wheel for Python 3.8") 50 | @pytest.mark.parametrize("flag", ["--no-wheel", "--wheel=none", "--wheel=embed", "--wheel=bundle"]) 51 | def test_wheel_cli_flags_dont_warn_on_38(tmp_path, flag, capsys): 52 | session_via_cli([flag, str(tmp_path)]) 53 | out, err = capsys.readouterr() 54 | assert "The --no-wheel and --wheel options are deprecated." not in out + err 55 | 56 | 57 | def test_embed_wheel_versions(tmp_path: Path) -> None: 58 | session = session_via_cli([str(tmp_path)]) 59 | if sys.version_info[:2] >= (3, 12): 60 | expected = {"pip": "bundle"} 61 | elif sys.version_info[:2] >= (3, 9): 62 | expected = {"pip": "bundle", "setuptools": "bundle"} 63 | else: 64 | expected = {"pip": "bundle", "setuptools": "bundle", "wheel": "bundle"} 65 | assert session.seeder.distribution_to_versions() == expected 66 | -------------------------------------------------------------------------------- /tests/unit/seed/embed/test_pip_invoke.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import itertools 4 | import sys 5 | from shutil import copy2 6 | 7 | import pytest 8 | 9 | from virtualenv.run import cli_run 10 | from virtualenv.seed.embed.pip_invoke import PipInvoke 11 | from virtualenv.seed.wheels.bundle import load_embed_wheel 12 | from virtualenv.seed.wheels.embed import BUNDLE_FOLDER, BUNDLE_SUPPORT 13 | 14 | 15 | @pytest.mark.slow 16 | @pytest.mark.parametrize("no", ["pip", "setuptools", "wheel", ""]) 17 | def test_base_bootstrap_via_pip_invoke(tmp_path, coverage_env, mocker, current_fastest, no): # noqa: C901 18 | extra_search_dir = tmp_path / "extra" 19 | extra_search_dir.mkdir() 20 | for_py_version = f"{sys.version_info.major}.{sys.version_info.minor}" 21 | new = BUNDLE_SUPPORT[for_py_version] 22 | for wheel_filename in BUNDLE_SUPPORT[for_py_version].values(): 23 | copy2(str(BUNDLE_FOLDER / wheel_filename), str(extra_search_dir)) 24 | 25 | def _load_embed_wheel(app_data, distribution, for_py_version, version): # noqa: ARG001 26 | return load_embed_wheel(app_data, distribution, old_ver, version) 27 | 28 | old_ver = "3.8" 29 | old = BUNDLE_SUPPORT[old_ver] 30 | mocker.patch("virtualenv.seed.wheels.bundle.load_embed_wheel", side_effect=_load_embed_wheel) 31 | 32 | def _execute(cmd, env): 33 | expected = set() 34 | for distribution, with_version in versions.items(): 35 | if distribution == no: 36 | continue 37 | if with_version == "embed" or old[distribution] == new[distribution]: 38 | expected.add(BUNDLE_FOLDER) 39 | else: 40 | expected.add(extra_search_dir) 41 | expected_list = list( 42 | itertools.chain.from_iterable(["--find-links", str(e)] for e in sorted(expected, key=str)), 43 | ) 44 | found = cmd[-len(expected_list) :] if expected_list else [] 45 | assert "--no-index" not in cmd 46 | cmd.append("--no-index") 47 | assert found == expected_list 48 | return original(cmd, env) 49 | 50 | original = PipInvoke._execute # noqa: SLF001 51 | run = mocker.patch.object(PipInvoke, "_execute", side_effect=_execute) 52 | versions = {"pip": "embed", "setuptools": "bundle"} 53 | if sys.version_info[:2] == (3, 8): 54 | versions["wheel"] = new["wheel"].split("-")[1] 55 | 56 | create_cmd = [ 57 | "--seeder", 58 | "pip", 59 | str(tmp_path / "env"), 60 | "--download", 61 | "--creator", 62 | current_fastest, 63 | "--extra-search-dir", 64 | str(extra_search_dir), 65 | "--app-data", 66 | str(tmp_path / "app-data"), 67 | ] 68 | for dist, version in versions.items(): 69 | create_cmd.extend([f"--{dist}", version]) 70 | if no: 71 | create_cmd.append(f"--no-{no}") 72 | result = cli_run(create_cmd) 73 | coverage_env() 74 | 75 | assert result 76 | assert run.call_count == 1 77 | 78 | site_package = result.creator.purelib 79 | pip = site_package / "pip" 80 | setuptools = site_package / "setuptools" 81 | wheel = site_package / "wheel" 82 | files_post_first_create = list(site_package.iterdir()) 83 | 84 | if no: 85 | no_file = locals()[no] 86 | assert no not in files_post_first_create 87 | 88 | for key in ("pip", "setuptools", "wheel"): 89 | if key == no: 90 | continue 91 | if sys.version_info[:2] >= (3, 9) and key == "wheel": 92 | continue 93 | assert locals()[key] in files_post_first_create 94 | -------------------------------------------------------------------------------- /tests/unit/seed/wheels/test_acquire_find_wheel.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from virtualenv.seed.wheels.acquire import find_compatible_in_house 6 | from virtualenv.seed.wheels.embed import BUNDLE_FOLDER, MAX, get_embed_wheel 7 | 8 | 9 | def test_find_latest_none(for_py_version): 10 | result = find_compatible_in_house("setuptools", None, for_py_version, BUNDLE_FOLDER) 11 | expected = get_embed_wheel("setuptools", for_py_version) 12 | assert result.path == expected.path 13 | 14 | 15 | def test_find_latest_string(for_py_version): 16 | result = find_compatible_in_house("setuptools", "", for_py_version, BUNDLE_FOLDER) 17 | expected = get_embed_wheel("setuptools", for_py_version) 18 | assert result.path == expected.path 19 | 20 | 21 | def test_find_exact(for_py_version): 22 | expected = get_embed_wheel("setuptools", for_py_version) 23 | result = find_compatible_in_house("setuptools", f"=={expected.version}", for_py_version, BUNDLE_FOLDER) 24 | assert result.path == expected.path 25 | 26 | 27 | def test_find_bad_spec(): 28 | with pytest.raises(ValueError, match="bad"): 29 | find_compatible_in_house("setuptools", "bad", MAX, BUNDLE_FOLDER) 30 | -------------------------------------------------------------------------------- /tests/unit/seed/wheels/test_bundle.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from datetime import datetime, timezone 5 | from pathlib import Path 6 | 7 | import pytest 8 | 9 | from virtualenv.app_data import AppDataDiskFolder 10 | from virtualenv.seed.wheels.bundle import from_bundle 11 | from virtualenv.seed.wheels.embed import get_embed_wheel 12 | from virtualenv.seed.wheels.periodic_update import dump_datetime 13 | from virtualenv.seed.wheels.util import Version, Wheel 14 | 15 | 16 | @pytest.fixture(scope="module") 17 | def next_pip_wheel(for_py_version): 18 | wheel = get_embed_wheel("pip", for_py_version) 19 | new_version = list(wheel.version_tuple) 20 | new_version[-1] += 1 21 | new_name = wheel.name.replace(wheel.version, ".".join(str(i) for i in new_version)) 22 | return Wheel.from_path(Path(new_name)) 23 | 24 | 25 | @pytest.fixture(scope="module") 26 | def app_data(tmp_path_factory, for_py_version, next_pip_wheel): 27 | temp_folder = tmp_path_factory.mktemp("module-app-data") 28 | now = dump_datetime(datetime.now(tz=timezone.utc)) 29 | app_data_ = AppDataDiskFolder(str(temp_folder)) 30 | app_data_.embed_update_log("pip", for_py_version).write( 31 | { 32 | "completed": now, 33 | "periodic": True, 34 | "started": now, 35 | "versions": [ 36 | { 37 | "filename": next_pip_wheel.name, 38 | "found_date": "2000-01-01T00:00:00.000000Z", 39 | "release_date": "2000-01-01T00:00:00.000000Z", 40 | "source": "periodic", 41 | }, 42 | ], 43 | }, 44 | ) 45 | return app_data_ 46 | 47 | 48 | def test_version_embed(app_data, for_py_version): 49 | wheel = from_bundle("pip", Version.embed, for_py_version, [], app_data, False, os.environ) 50 | assert wheel is not None 51 | assert wheel.name == get_embed_wheel("pip", for_py_version).name 52 | 53 | 54 | def test_version_bundle(app_data, for_py_version, next_pip_wheel): 55 | wheel = from_bundle("pip", Version.bundle, for_py_version, [], app_data, False, os.environ) 56 | assert wheel is not None 57 | assert wheel.name == next_pip_wheel.name 58 | 59 | 60 | def test_version_pinned_not_found(app_data, for_py_version): 61 | wheel = from_bundle("pip", "0.0.0", for_py_version, [], app_data, False, os.environ) 62 | assert wheel is None 63 | 64 | 65 | def test_version_pinned_is_embed(app_data, for_py_version): 66 | expected_wheel = get_embed_wheel("pip", for_py_version) 67 | wheel = from_bundle("pip", expected_wheel.version, for_py_version, [], app_data, False, os.environ) 68 | assert wheel is not None 69 | assert wheel.name == expected_wheel.name 70 | 71 | 72 | def test_version_pinned_in_app_data(app_data, for_py_version, next_pip_wheel): 73 | wheel = from_bundle("pip", next_pip_wheel.version, for_py_version, [], app_data, False, os.environ) 74 | assert wheel is not None 75 | assert wheel.name == next_pip_wheel.name 76 | -------------------------------------------------------------------------------- /tests/unit/seed/wheels/test_wheels_util.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from virtualenv.seed.wheels.embed import MAX, get_embed_wheel 6 | from virtualenv.seed.wheels.util import Wheel 7 | 8 | 9 | def test_wheel_support_no_python_requires(mocker): 10 | wheel = get_embed_wheel("setuptools", for_py_version=None) 11 | zip_mock = mocker.MagicMock() 12 | mocker.patch("virtualenv.seed.wheels.util.ZipFile", new=zip_mock) 13 | zip_mock.return_value.__enter__.return_value.read = lambda name: b"" # noqa: ARG005 14 | 15 | supports = wheel.support_py("3.8") 16 | assert supports is True 17 | 18 | 19 | def test_bad_as_version_tuple(): 20 | with pytest.raises(ValueError, match="bad"): 21 | Wheel.as_version_tuple("bad") 22 | 23 | 24 | def test_wheel_not_support(): 25 | wheel = get_embed_wheel("setuptools", MAX) 26 | assert wheel.support_py("3.3") is False 27 | 28 | 29 | def test_wheel_repr(): 30 | wheel = get_embed_wheel("setuptools", MAX) 31 | assert str(wheel.path) in repr(wheel) 32 | 33 | 34 | def test_unknown_distribution(): 35 | wheel = get_embed_wheel("unknown", MAX) 36 | assert wheel is None 37 | -------------------------------------------------------------------------------- /tests/unit/test_run.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | 5 | import pytest 6 | 7 | from virtualenv import __version__ 8 | from virtualenv.run import cli_run, session_via_cli 9 | 10 | 11 | def test_help(capsys): 12 | with pytest.raises(SystemExit) as context: 13 | cli_run(args=["-h", "-vvv"]) 14 | assert context.value.code == 0 15 | 16 | out, err = capsys.readouterr() 17 | assert not err 18 | assert out 19 | 20 | 21 | def test_version(capsys): 22 | with pytest.raises(SystemExit) as context: 23 | cli_run(args=["--version"]) 24 | assert context.value.code == 0 25 | 26 | content, err = capsys.readouterr() 27 | assert not err 28 | 29 | assert __version__ in content 30 | import virtualenv # noqa: PLC0415 31 | 32 | assert virtualenv.__file__ in content 33 | 34 | 35 | @pytest.mark.parametrize("on", [True, False]) 36 | def test_logging_setup(caplog, on): 37 | caplog.set_level(logging.DEBUG) 38 | session_via_cli(["env"], setup_logging=on) 39 | # DEBUG only level output is generated during this phase, default output is WARN, so if on no records should be 40 | if on: 41 | assert not caplog.records 42 | else: 43 | assert caplog.records 44 | -------------------------------------------------------------------------------- /tests/unit/test_util.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import concurrent.futures 4 | import traceback 5 | 6 | import pytest 7 | 8 | from virtualenv.util.lock import ReentrantFileLock 9 | from virtualenv.util.subprocess import run_cmd 10 | 11 | 12 | def test_run_fail(tmp_path): 13 | code, out, err = run_cmd([str(tmp_path)]) 14 | assert err 15 | assert not out 16 | assert code 17 | 18 | 19 | def test_reentrant_file_lock_is_thread_safe(tmp_path): 20 | lock = ReentrantFileLock(tmp_path) 21 | target_file = tmp_path / "target" 22 | target_file.touch() 23 | 24 | def recreate_target_file(): 25 | with lock.lock_for_key("target"): 26 | target_file.unlink() 27 | target_file.touch() 28 | 29 | with concurrent.futures.ThreadPoolExecutor() as executor: 30 | tasks = [executor.submit(recreate_target_file) for _ in range(4)] 31 | concurrent.futures.wait(tasks) 32 | for task in tasks: 33 | try: 34 | task.result() 35 | except Exception: # noqa: BLE001, PERF203 36 | pytest.fail(traceback.format_exc()) 37 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4.2 4 | env_list = 5 | fix 6 | pypy3 7 | 3.13 8 | 3.12 9 | 3.11 10 | 3.10 11 | 3.9 12 | 3.8 13 | graalpy 14 | coverage 15 | readme 16 | docs 17 | 3.13t 18 | skip_missing_interpreters = true 19 | 20 | [testenv] 21 | description = run tests with {basepython} 22 | package = wheel 23 | wheel_build_env = .pkg 24 | extras = 25 | test 26 | pass_env = 27 | CI_RUN 28 | PYTEST_* 29 | TERM 30 | set_env = 31 | COVERAGE_FILE = {toxworkdir}/.coverage.{envname} 32 | COVERAGE_PROCESS_START = {toxinidir}/pyproject.toml 33 | PYTHONWARNDEFAULTENCODING = 1 34 | _COVERAGE_SRC = {envsitepackagesdir}/virtualenv 35 | commands = 36 | !graalpy: coverage erase 37 | !graalpy: coverage run -m pytest {posargs:--junitxml "{toxworkdir}/junit.{envname}.xml" tests --int} 38 | !graalpy: coverage combine 39 | !graalpy: coverage report --skip-covered --show-missing 40 | !graalpy: coverage xml -o "{toxworkdir}/coverage.{envname}.xml" 41 | !graalpy: coverage html -d {envtmpdir}/htmlcov --show-contexts --title virtualenv-{envname}-coverage 42 | graalpy: pytest {posargs:--junitxml "{toxworkdir}/junit.{envname}.xml" tests --skip-slow} 43 | uv_seed = true 44 | 45 | [testenv:fix] 46 | description = format the code base to adhere to our styles, and complain about what we cannot do automatically 47 | skip_install = true 48 | deps = 49 | pre-commit-uv>=4.1.1 50 | commands = 51 | pre-commit run --all-files --show-diff-on-failure 52 | 53 | [testenv:readme] 54 | description = check that the long description is valid 55 | skip_install = true 56 | deps = 57 | check-wheel-contents>=0.6 58 | twine>=5.1.1 59 | uv>=0.4.10 60 | commands = 61 | uv build --sdist --wheel --out-dir {envtmpdir} . 62 | twine check {envtmpdir}{/}* 63 | check-wheel-contents --no-config {envtmpdir} 64 | 65 | [testenv:docs] 66 | description = build documentation 67 | extras = 68 | docs 69 | commands = 70 | sphinx-build -d "{envtmpdir}/doctree" docs "{toxworkdir}/docs_out" --color -b html {posargs:-W} 71 | python -c 'import pathlib; print("documentation available under file://\{0\}".format(pathlib.Path(r"{toxworkdir}") / "docs_out" / "index.html"))' 72 | 73 | [testenv:3.13t] 74 | base_python = {env:TOX_BASEPYTHON} 75 | 76 | [testenv:upgrade] 77 | description = upgrade pip/wheels/setuptools to latest 78 | skip_install = true 79 | deps = 80 | ruff>=0.6.5 81 | pass_env = 82 | UPGRADE_ADVISORY 83 | change_dir = {toxinidir}/tasks 84 | commands = 85 | - python upgrade_wheels.py 86 | uv_seed = true 87 | 88 | [testenv:release] 89 | description = do a release, required posarg of the version number 90 | deps = 91 | gitpython>=3.1.43 92 | packaging>=24.1 93 | towncrier>=24.8 94 | change_dir = {toxinidir}/tasks 95 | commands = 96 | python release.py --version {posargs} 97 | 98 | [testenv:dev] 99 | description = generate a DEV environment 100 | package = editable 101 | extras = 102 | docs 103 | test 104 | commands = 105 | uv pip tree 106 | python -c 'import sys; print(sys.executable)' 107 | 108 | [testenv:zipapp] 109 | description = generate a zipapp 110 | skip_install = true 111 | deps = 112 | packaging>=24.1 113 | commands = 114 | python tasks/make_zipapp.py 115 | uv_seed = true 116 | --------------------------------------------------------------------------------