diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 2fe44d67a..000000000 --- a/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -.tox -pip-wheel-metadata -venv* -*.pyz diff --git a/.gitattributes b/.gitattributes index c925e269f..86c9df405 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1 @@ -*.bat text eol=crlf -*.ps1 text eol=lf -*.fish text eol=lf -*.csh text eol=lf -*.sh text eol=lf +virtualenv_embedded/*.bat text eol=crlf \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 22b635337..000000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,11 +0,0 @@ -# Contributing to `virtualenv` - -Thank you for your interest in contributing to virtualenv! There are many ways to contribute, and we appreciate all of -them. As a reminder, all contributors are expected to follow the [PSF Code of Conduct][coc]. - -[coc]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md - -## Development Documentation - -Our [development documentation](https://virtualenv.pypa.io/en/latest/development.html#development) contains details on -how to get started with contributing to `virtualenv`, and details of our development processes. diff --git a/.github/FUNDING.yaml b/.github/FUNDING.yaml deleted file mode 100644 index 91b483e4e..000000000 --- a/.github/FUNDING.yaml +++ /dev/null @@ -1 +0,0 @@ -tidelift: "pypi/virtualenv" diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index 2a1b732f7..000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "" -labels: bug -assignees: "" ---- - -**Issue** - -Describe what's the expected behavior and what you're observing. - -**Environment** - -Provide at least: - -- OS: -- Shell: -- Python version and path: -- `pip list` of the host python where `virtualenv` is installed: - - ```console - - ``` - -**Output of the virtual environment creation** - -Make sure to run the creation with `-vvv --with-traceback`: - -```console - -``` diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 1cc9cf633..000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser -blank_issues_enabled: true # default -contact_links: - - name: "๐Ÿ’ฌ pypa/virtualenv @ Discord" - url: https://discord.gg/pypa - about: Chat with the devs - - name: ๐Ÿคท๐Ÿ’ป๐Ÿคฆ Discourse - url: https://discuss.python.org/c/packaging - about: | - Please ask typical Q&A here: general ideas for Python packaging, questions about structuring projects and so on - - name: ๐Ÿ“ PSF Code of Conduct - url: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md - about: โค Be nice to other members of the community. โ˜ฎ Behave. diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md deleted file mode 100644 index 8714ceb6e..000000000 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: Feature request -about: Suggest an enhancement for this project -title: "" -labels: enhancement -assignees: "" ---- - -**What's the problem this feature will solve?** - - - -**Describe the solution you'd like** - - - - - -**Alternative Solutions** - - - -**Additional context** - - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index ae6ea93a0..000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,7 +0,0 @@ -### 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)) - -- [ ] ran the linter to address style issues (`tox -e fix`) -- [ ] wrote descriptive pull request text -- [ ] ensured there are test(s) validating the fix -- [ ] added news fragment in `docs/changelog` folder -- [ ] updated/extended the documentation diff --git a/.github/SECURITY.md b/.github/SECURITY.md deleted file mode 100644 index 90836425e..000000000 --- a/.github/SECURITY.md +++ /dev/null @@ -1,13 +0,0 @@ -# Security Policy - -## Supported Versions - -| Version | Supported | -| --------- | ------------------ | -| 20.15.1 + | :white_check_mark: | -| < 20.15.1 | :x: | - -## Reporting a Vulnerability - -To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift -will coordinate the fix and disclosure. diff --git a/.github/config.yml b/.github/config.yml deleted file mode 100644 index 74a0ed9f7..000000000 --- a/.github/config.yml +++ /dev/null @@ -1,2 +0,0 @@ -rtd: - project: virtualenv diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml deleted file mode 100644 index 123014908..000000000 --- a/.github/dependabot.yaml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" diff --git a/.github/release.yaml b/.github/release.yaml deleted file mode 100644 index 5f89818f9..000000000 --- a/.github/release.yaml +++ /dev/null @@ -1,5 +0,0 @@ -changelog: - exclude: - authors: - - dependabot[bot] - - pre-commit-ci[bot] diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml deleted file mode 100644 index 6fb3988fa..000000000 --- a/.github/workflows/check.yaml +++ /dev/null @@ -1,196 +0,0 @@ -name: ๐Ÿงช check -on: - workflow_dispatch: - push: - branches: ["main"] - pull_request: - schedule: - - cron: "0 8 * * *" - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - test: - name: ๐Ÿงช test ${{ matrix.py }} - ${{ matrix.os }} - if: github.event_name != 'schedule' || github.repository_owner == 'pypa' - runs-on: ${{ matrix.os }} - timeout-minutes: 40 - permissions: - contents: read - strategy: - fail-fast: false - matrix: - py: - - "3.14t" - - "3.14" - - "3.13t" - - "3.13" - - "3.12" - - "3.11" - - "3.10" - - "3.9" - - "3.8" - - pypy-3.11 - - pypy-3.10 - - pypy-3.9 - - pypy-3.8 - - graalpy-24.1 - - rp - os: - - ubuntu-24.04 - - macos-15 - - windows-2025 - include: - - { os: macos-15, py: "brew@3.14" } - - { os: macos-15, py: "brew@3.13" } - - { os: macos-15, py: "brew@3.12" } - - { os: macos-15, py: "brew@3.11" } - - { os: macos-15, py: "brew@3.10" } - - { os: macos-15, py: "brew@3.9" } - exclude: - - { os: windows-2025, py: "graalpy-24.1" } - - { os: windows-2025, py: "pypy-3.10" } - - { os: windows-2025, py: "pypy-3.9" } - - { os: windows-2025, py: "pypy-3.8" } - steps: - - name: ๐Ÿš€ Install uv - uses: astral-sh/setup-uv@v7 - - name: ๐Ÿ“ฅ Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: ๐Ÿท๏ธ Fetch upstream tags for versioning - shell: bash - run: | - git fetch --force --tags https://github.com/pypa/virtualenv.git - - name: ๐Ÿ Setup Python for tox - uses: actions/setup-python@v6 - with: - python-version: "3.14" - - name: ๐Ÿ“ฆ Install tox with this virtualenv - shell: bash - run: | - if [[ "${{ matrix.py }}" == "3.13t" || "${{ matrix.py }}" == "3.14t" || "${{ matrix.py }}" == "rp" ]]; then - uv tool install --no-managed-python --python 3.14 "tox>=4.45" --with . - else - uv tool install --no-managed-python --python 3.14 "tox>=4.45" --with tox-uv --with . - fi - - name: ๐Ÿ Setup Python for test ${{ matrix.py }} - uses: actions/setup-python@v6 - if: ${{ !startsWith(matrix.py, 'brew@') && matrix.py != 'rp' }} - with: - python-version: ${{ matrix.py }} - - name: ๐Ÿฆ€ Setup RustPython - if: matrix.py == 'rp' - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - tag=$(gh release list --repo RustPython/RustPython --limit 1 --json tagName -q '.[0].tagName') - if [ "${{ runner.os }}" = "Linux" ]; then - asset="rustpython-release-Linux-x86_64-unknown-linux-gnu" - elif [ "${{ runner.os }}" = "macOS" ]; then - asset="rustpython-release-macOS-aarch64-apple-darwin" - elif [ "${{ runner.os }}" = "Windows" ]; then - asset="rustpython-release-Windows-x86_64-pc-windows-msvc.exe" - fi - gh release download "$tag" --repo RustPython/RustPython --pattern "$asset" --dir "$RUNNER_TEMP" - if [ "${{ runner.os }}" = "Windows" ]; then - cp "$RUNNER_TEMP/$asset" "/c/ProgramData/chocolatey/bin/rustpython.exe" - else - chmod +x "$RUNNER_TEMP/$asset" - sudo mv "$RUNNER_TEMP/$asset" /usr/local/bin/rustpython - fi - - name: ๐Ÿ› ๏ธ Install OS dependencies - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - if [ "${{ runner.os }}" = "Linux" ]; then - sudo apt-get install -y software-properties-common - sudo apt-add-repository ppa:fish-shell/release-4 -y - curl -fsSL https://apt.fury.io/nushell/gpg.key | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/fury-nushell.gpg - echo "deb https://apt.fury.io/nushell/ /" | sudo tee /etc/apt/sources.list.d/fury.list - sudo apt-get update -y - sudo apt-get install snapd fish csh nushell -y - elif [ "${{ runner.os }}" = "macOS" ]; then - brew update - if [[ "${{ matrix.py }}" == brew@* ]]; then - PY=$(echo '${{ matrix.py }}' | cut -c 6-) - brew install python@$PY || brew upgrade python@$PY - echo "/usr/local/opt/python@$PY/libexec/bin" >>"${GITHUB_PATH}" - fi - brew install fish tcsh nushell || brew upgrade fish tcsh nushell - elif [ "${{ runner.os }}" = "Windows" ]; then - nu_version=$(gh release view --repo nushell/nushell --json tagName -q .tagName) - gh release download "$nu_version" --repo nushell/nushell --pattern "*-x86_64-pc-windows-msvc.zip" --dir "$RUNNER_TEMP" - 7z x "$RUNNER_TEMP/nu-${nu_version#v}-x86_64-pc-windows-msvc.zip" -o"$RUNNER_TEMP/nushell" -y - cp "$RUNNER_TEMP/nushell/nu.exe" "/c/ProgramData/chocolatey/bin/nu.exe" - fi - - name: ๐Ÿงฌ Pick environment to run - shell: bash - run: | - py="${{ matrix.py }}" - if [[ "$py" == brew@* ]]; then - brew_version="${py#brew@}" - echo "UV_PYTHON=/opt/homebrew/bin/python${brew_version}" >> "$GITHUB_ENV" - py="$brew_version" - fi - [[ "$py" == graalpy-* ]] && py="graalpy" - echo "TOXENV=$py" >> "$GITHUB_ENV" - echo "Set TOXENV=$py" - - name: ๐Ÿ—๏ธ Setup test suite - run: tox run -vvvv --notest --skip-missing-interpreters false - - name: ๐Ÿƒ Run test suite - run: tox run --skip-pkg-install - timeout-minutes: 30 - env: - PYTEST_ADDOPTS: "-vv --durations=20" - CI_RUN: "yes" - DIFF_AGAINST: HEAD - check: - name: ๐Ÿ”Ž check ${{ matrix.tox_env }} - ${{ matrix.os }} - if: github.event_name != 'schedule' || github.repository_owner == 'pypa' - runs-on: ${{ matrix.os }} - permissions: - contents: read - strategy: - fail-fast: false - matrix: - os: - - ubuntu-24.04 - - windows-2025 - tox_env: - - dev - - docs - - readme - - type - - type-3.8 - - upgrade - - zipapp - exclude: - - { os: windows-2025, tox_env: docs } - - { os: windows-2025, tox_env: readme } - - { os: windows-2025, tox_env: type } - - { os: windows-2025, tox_env: type-3.8 } - steps: - - name: ๐Ÿš€ Install uv - uses: astral-sh/setup-uv@v7 - - name: ๐Ÿ“ฆ Install tox - run: uv tool install --python-preference only-managed --python 3.14 "tox>=4.45" --with tox-uv - - name: ๐Ÿ“ฅ Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: ๐Ÿท๏ธ Fetch upstream tags for versioning - shell: bash - run: | - git fetch --force --tags https://github.com/pypa/virtualenv.git - - name: ๐Ÿ—๏ธ Setup check suite - run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.tox_env }} - - name: ๐Ÿƒ Run check for ${{ matrix.tox_env }} - run: tox run --skip-pkg-install -e ${{ matrix.tox_env }} - env: - UPGRADE_ADVISORY: ${{ matrix.tox_env == 'upgrade' && '1' || '' }} diff --git a/.github/workflows/pre-release.yaml b/.github/workflows/pre-release.yaml deleted file mode 100644 index afadb20f9..000000000 --- a/.github/workflows/pre-release.yaml +++ /dev/null @@ -1,44 +0,0 @@ -name: Pre-release -on: - workflow_dispatch: - inputs: - bump: - description: "Version bump type" - required: true - type: choice - options: - - auto - - major - - minor - - patch - default: auto - -jobs: - pre-release: - runs-on: ubuntu-24.04 - environment: release - permissions: - contents: write - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - token: ${{ secrets.GH_RELEASE_TOKEN }} - - name: Install the latest version of uv - uses: astral-sh/setup-uv@v7 - with: - enable-cache: true - cache-dependency-glob: "pyproject.toml" - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Set up Python - run: uv python install 3.14 - - name: Configure git identity from token owner - env: - GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} - run: | - user_info=$(gh api /user) - git config user.name "$(echo "$user_info" | jq -r '.name // .login')" - git config user.email "$(echo "$user_info" | jq -r '.id')+$(echo "$user_info" | jq -r '.login')@users.noreply.github.com" - - name: Generate changelog, commit, tag, and push - run: uv tool run --with tox-uv tox r -e release -- --version "${{ inputs.bump }}" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index 2cb9198ae..000000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,123 +0,0 @@ -name: Release -on: - push: - tags: ["*.*.*"] - -env: - dists-artifact-name: python-package-distributions - -jobs: - build: - runs-on: ubuntu-24.04 - permissions: - contents: read - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Install the latest version of uv - uses: astral-sh/setup-uv@v7 - with: - enable-cache: true - cache-dependency-glob: "pyproject.toml" - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Set up Python - run: uv python install 3.14 - - name: Build sdist and wheel - run: uv build --python 3.14 --python-preference only-managed --sdist --wheel . --out-dir dist - - name: Build zipapp - run: uv tool run --with tox-uv tox r -e zipapp - - name: Store the distribution packages - uses: actions/upload-artifact@v7 - with: - name: ${{ env.dists-artifact-name }} - path: dist/* - - name: Store the zipapp - uses: actions/upload-artifact@v7 - with: - name: virtualenv-zipapp - path: virtualenv.pyz - - publish: - needs: build - runs-on: ubuntu-24.04 - environment: - name: release - url: https://pypi.org/project/virtualenv/${{ github.ref_name }} - permissions: - contents: write - id-token: write - steps: - - name: Download all the dists - uses: actions/download-artifact@v8 - with: - name: ${{ env.dists-artifact-name }} - path: dist/ - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.13.0 - with: - attestations: true - - name: Download the zipapp - uses: actions/download-artifact@v8 - with: - name: virtualenv-zipapp - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ github.ref_name }} - generate_release_notes: true - files: virtualenv.pyz - - name: Update get-virtualenv - env: - GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} - run: | - git clone https://x-access-token:${GH_TOKEN}@github.com/pypa/get-virtualenv.git - cp virtualenv.pyz get-virtualenv/public/virtualenv.pyz - echo -n "${{ github.ref_name }}" > get-virtualenv/public/version.txt - cd get-virtualenv - user_info=$(gh api /user) - git config user.name "$(echo "$user_info" | jq -r '.name // .login')" - git config user.email "$(echo "$user_info" | jq -r '.id')+$(echo "$user_info" | jq -r '.login')@users.noreply.github.com" - git add public/virtualenv.pyz public/version.txt - git commit -m "update virtualenv to ${{ github.ref_name }}" - git push origin main - - rollback: - if: ${{ always() && needs.build.result == 'success' && needs.publish.result == 'failure' }} - needs: - - build - - publish - runs-on: ubuntu-24.04 - environment: release - permissions: - contents: write - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - token: ${{ secrets.GH_RELEASE_TOKEN }} - - name: Delete GitHub Release if created - env: - GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} - run: gh release delete "${{ github.ref_name }}" --yes --cleanup-tag || true - - name: Delete remote tag - run: git push origin --delete "${{ github.ref_name }}" || true - - name: Reset release commit on main - run: | - git checkout main - git reset --hard HEAD~1 - git push origin main --force - - name: Rollback get-virtualenv if updated - env: - GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} - run: | - git clone https://x-access-token:${GH_TOKEN}@github.com/pypa/get-virtualenv.git - cd get-virtualenv - current_version=$(cat public/version.txt) - if [ "$current_version" = "${{ github.ref_name }}" ]; then - gh release delete "$current_version" --yes --cleanup-tag || true - git reset --hard HEAD~1 - git push origin main --force - fi diff --git a/.github/workflows/upgrade.yaml b/.github/workflows/upgrade.yaml deleted file mode 100644 index 189fe3842..000000000 --- a/.github/workflows/upgrade.yaml +++ /dev/null @@ -1,94 +0,0 @@ -name: Upgrade embedded dependencies -on: - schedule: - - cron: "0 10 * * *" # daily at 10:00 UTC - workflow_dispatch: - -permissions: - contents: read - -jobs: - upgrade: - name: Upgrade embedded pip/setuptools/wheel - if: github.repository_owner == 'pypa' - runs-on: ubuntu-24.04 - permissions: - contents: write - pull-requests: write - steps: - - name: Install uv - uses: astral-sh/setup-uv@v7 - - name: Install tox - run: uv tool install --python-preference only-managed --python 3.14 "tox>=4.32" --with tox-uv - - name: Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - ssh-key: ${{ secrets.DEPLOY_KEY }} - - name: Fetch upstream tags for versioning - run: git fetch --force --tags https://github.com/pypa/virtualenv.git - - name: Run upgrade - id: upgrade - env: - UPGRADE_ADVISORY: "1" - run: | - tox run -e upgrade - if [ -n "$(git diff --name-only)" ]; then - echo "changed=true" >> "$GITHUB_OUTPUT" - echo "### Embedded dependency changes" >> "$GITHUB_STEP_SUMMARY" - git diff --stat >> "$GITHUB_STEP_SUMMARY" - else - echo "changed=false" >> "$GITHUB_OUTPUT" - echo "### All embedded dependencies are up to date" >> "$GITHUB_STEP_SUMMARY" - fi - - name: Check PyPI release age - if: steps.upgrade.outputs.changed == 'true' - id: age-check - run: | - cutoff=$(( $(date +%s) - 7 * 24 * 3600 )) - too_new=false - for whl in $(git diff --name-only -- 'src/virtualenv/seed/wheels/embed/*.whl'); do - pkg=$(basename "$whl" | cut -d- -f1) - version=$(basename "$whl" | cut -d- -f2) - upload_time=$(curl -sf "https://pypi.org/pypi/${pkg}/${version}/json" | jq -r '.urls[0].upload_time_iso_8601 // empty') - if [ -z "$upload_time" ]; then - echo "::warning::Could not fetch release date for ${pkg}==${version}" - continue - fi - release_epoch=$(date -d "$upload_time" +%s) - if [ "$release_epoch" -gt "$cutoff" ]; then - echo "::notice::${pkg}==${version} released less than 7 days ago (${upload_time})" - too_new=true - fi - done - if [ "$too_new" = "true" ]; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "### Skipped โ€” some packages released less than 7 days ago" >> "$GITHUB_STEP_SUMMARY" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - name: Create Pull Request - if: steps.upgrade.outputs.changed == 'true' && steps.age-check.outputs.skip != 'true' - id: cpr - uses: peter-evans/create-pull-request@v8 - with: - commit-message: "Upgrade embedded dependencies" - branch: auto/upgrade-embedded-deps - delete-branch: true - title: "Upgrade embedded pip/setuptools/wheel" - body: | - Automated upgrade of embedded pip, setuptools, and wheel dependencies. - - This PR was created automatically by the [upgrade workflow](https://github.com/${{ github.repository }}/actions/workflows/upgrade.yaml). - labels: | - dependencies - - name: Rename changelog with PR number - if: steps.cpr.outputs.pull-request-number - run: | - pr_number="${{ steps.cpr.outputs.pull-request-number }}" - git fetch origin auto/upgrade-embedded-deps - git checkout auto/upgrade-embedded-deps - mv docs/changelog/u.bugfix.rst "docs/changelog/${pr_number}.bugfix.rst" - git add docs/changelog/ - git commit -m "Rename changelog to ${pr_number}.bugfix.rst" - git push diff --git a/.gitignore b/.gitignore index b799ed285..5f93150b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,11 @@ -# packaging -*.egg-info +virtualenv.egg-info build dist +docs/_build +.DS_Store +*.py[cod] *.egg .eggs - -# python -*.py[codz] -*$py.class - -# tools .tox -.*_cache -.DS_Store - -# IDE -.idea -.vscode - -/docs/_draft.rst -/pip-wheel-metadata -/src/virtualenv/version.py -/src/virtualenv/out -.python-version - -*wheel-store* - -Dockerfile* -.dockerignore +.cache +tests/test_activate_actual.output diff --git a/.markdownlint.yaml b/.markdownlint.yaml deleted file mode 100644 index 33a1615f7..000000000 --- a/.markdownlint.yaml +++ /dev/null @@ -1,10 +0,0 @@ -MD013: - code_blocks: false - headers: false - line_length: 120 - tables: false - -MD046: - style: fenced -no-emphasis-as-header: false -first-line-h1: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index d640d7271..000000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,47 +0,0 @@ -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 - hooks: - - id: end-of-file-fixer - - id: trailing-whitespace - - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.37.0 - hooks: - - id: check-github-workflows - args: ["--verbose"] - - repo: https://github.com/codespell-project/codespell - rev: v2.4.2 - hooks: - - id: codespell - args: ["--write-changes"] - - repo: https://github.com/tox-dev/tox-toml-fmt - rev: "v1.9.1" - hooks: - - id: tox-toml-fmt - - repo: https://github.com/tox-dev/pyproject-fmt - rev: "v2.20.0" - hooks: - - id: pyproject-fmt - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.15.7" - hooks: - - id: ruff-format - - id: ruff - args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] - - repo: https://github.com/rbubley/mirrors-prettier - rev: "v3.8.1" - hooks: - - id: prettier - additional_dependencies: - - prettier@3.3.3 - - "@prettier/plugin-xml@3.4.1" - - repo: https://github.com/LilSpazJoekp/docstrfmt - rev: v2.0.2 - hooks: - - id: docstrfmt - args: ["-l", "120"] - additional_dependencies: ["sphinx>=9.1"] - - repo: meta - hooks: - - id: check-hooks-apply - - id: check-useless-excludes diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index 60e9a43d0..000000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: 2 -build: - os: ubuntu-lts-latest - tools: {} - commands: - - curl -LsSf https://astral.sh/uv/install.sh | sh - - ~/.local/bin/uv tool install tox --with tox-uv -p 3.14 --managed-python - - ~/.local/bin/tox run -e docs -- diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..676e5b5dc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,41 @@ +language: python + +matrix: + include: + - python: 2.6 + env: TOXENV=python2.6 + - python: 2.7 + env: TOXENV=python2.7 + - python: 3.3 + env: TOXENV=python3.3 + - python: 3.4 + env: TOXENV=python3.4 + - python: 3.5 + env: TOXENV=python3.5 + - python: pypy + env: TOXENV=pypy + - python: 3.5 + env: TOXENV=crosspython3 + - python: 2.7 + env: TOXENV=crosspython2 + - python: 2.7 + env: TOXENV=docs + +install: pip install tox + +script: tox + +sudo: false # Use container based builds + +branches: + only: + - master + - develop + - rewrite + +notifications: + irc: + channels: + - "irc.freenode.org#pypa-dev" + use_notice: true + skip_join: true diff --git a/AUTHORS.txt b/AUTHORS.txt new file mode 100644 index 000000000..272494163 --- /dev/null +++ b/AUTHORS.txt @@ -0,0 +1,91 @@ +Author +------ + +Ian Bicking + +Maintainers +----------- + +Brian Rosner +Carl Meyer +Jannis Leidel +Paul Moore +Paul Nasrat +Marcus Smith + +Contributors +------------ + +Alex Grรถnholm +Anatoly Techtonik +Antonio Cuni +Antonio Valentino +Armin Ronacher +Barry Warsaw +Benjamin Root +Bradley Ayers +Branden Rolston +Brandon Carl +Brian Kearns +Cap Petschulat +CBWhiz +Chris Adams +Chris McDonough +Christos Kontas +Christian Hudon +Christian Stefanescu +Christopher Nilsson +Cliff Xuan +Curt Micol +Damien Nozay +Dan Sully +Daniel Hahler +Daniel Holth +David Schoonover +Denis Costa +Doug Hellmann +Doug Napoleone +Douglas Creager +Eduard-Cristian Stefan +Erik M. Bray +Ethan Jucovy +Gabriel de Perthuis +Gunnlaugur Thor Briem +Graham Dennis +Greg Haskins +Jason Penney +Jason R. Coombs +Jeff Hammel +Jeremy Orem +Jason Penney +Jason R. Coombs +John Kleint +Jonathan Griffin +Jonathan Hitchcock +Jorge Vargas +Josh Bronson +Kamil Kisiel +Kyle Gibson +Konstantin Zemlyak +Kumar McMillan +Lars Francke +Marc Abramowitz +Mika Laitio +Mike Hommey +Miki Tebeka +Philip Jenvey +Philippe Ombredanne +Piotr Dobrogost +Preston Holmes +Ralf Schmitt +Raul Leal +Ronny Pfannschmidt +Satrajit Ghosh +Sergio de Carvalho +Stefano Rivera +Tarek Ziadรฉ +Thomas Aglassinger +Vinay Sajip +Vitaly Babiy +Vladimir Rutsky +Wang Xuerui \ No newline at end of file diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 000000000..924e7e282 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,21 @@ +virtualenv +========== + +See docs/index.rst for user documentation. + +Contributor notes +----------------- + +* virtualenv is designed to work on python 2 and 3 with a single code base. + Use Python 3 print-function syntax, and always ``use sys.exc_info()[1]`` + inside the ``except`` block to get at exception objects. + +* virtualenv uses git-flow_ to `coordinate development`_. The latest stable + version should exist on the *master* branch, and new work should be + integrated to *develop*. + +* All changes to files inside virtualenv_embedded should be integrated to + ``virtualenv.py`` with ``bin/rebuild-script.py``. + +.. _git-flow: https://github.com/nvie/gitflow +.. _coordinate development: http://nvie.com/posts/a-successful-git-branching-model/ diff --git a/LICENSE b/LICENSE.txt similarity index 86% rename from LICENSE rename to LICENSE.txt index be9700d61..ab145001f 100644 --- a/LICENSE +++ b/LICENSE.txt @@ -1,4 +1,6 @@ -Copyright (c) 2020-202x The virtualenv developers +Copyright (c) 2007 Ian Bicking and Contributors +Copyright (c) 2009 Ian Bicking, The Open Planning Project +Copyright (c) 2011-2016 The virtualenv developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..49037ada6 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,12 @@ +recursive-include docs * +recursive-include tests *.py *.sh *.expected +recursive-include virtualenv_support *.whl +recursive-include virtualenv_embedded * +recursive-exclude docs/_templates * +recursive-exclude docs/_build * +include virtualenv_support/__init__.py +include bin/* +include scripts/* +include *.py +include AUTHORS.txt +include LICENSE.txt diff --git a/README.md b/README.md deleted file mode 100644 index 87f45e077..000000000 --- a/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# virtualenv - -[![PyPI](https://img.shields.io/pypi/v/virtualenv?style=flat-square)](https://pypi.org/project/virtualenv) -[![PyPI - Implementation](https://img.shields.io/pypi/implementation/virtualenv?style=flat-square)](https://pypi.org/project/virtualenv) -[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/virtualenv?style=flat-square)](https://pypi.org/project/virtualenv) -[![Documentation](https://readthedocs.org/projects/virtualenv/badge/?version=latest&style=flat-square)](http://virtualenv.pypa.io) -[![Discord](https://img.shields.io/discord/803025117553754132)](https://discord.gg/pypa) -[![Downloads](https://static.pepy.tech/badge/virtualenv/month)](https://pepy.tech/project/virtualenv) -[![PyPI - License](https://img.shields.io/pypi/l/virtualenv?style=flat-square)](https://opensource.org/licenses/MIT) -[![check](https://github.com/pypa/virtualenv/actions/workflows/check.yaml/badge.svg)](https://github.com/pypa/virtualenv/actions/workflows/check.yaml) - -A tool for creating isolated `virtual` python environments. - -- [Installation](https://virtualenv.pypa.io/en/latest/installation.html) -- [Documentation](https://virtualenv.pypa.io) -- [Changelog](https://virtualenv.pypa.io/en/latest/changelog.html) -- [Issues](https://github.com/pypa/virtualenv/issues) -- [PyPI](https://pypi.org/project/virtualenv) -- [Github](https://github.com/pypa/virtualenv) - -## Code of Conduct - -Everyone interacting in the virtualenv project's codebases, issue trackers, chat rooms, and mailing lists is expected to -follow the [PSF Code of Conduct](https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md). diff --git a/README.rst b/README.rst new file mode 100644 index 000000000..0d5984dce --- /dev/null +++ b/README.rst @@ -0,0 +1,31 @@ +virtualenv +========== + +A tool for creating isolated 'virtual' python environments. + +.. image:: https://img.shields.io/pypi/v/virtualenv.svg + :target: https://pypi.python.org/pypi/virtualenv + +.. image:: https://img.shields.io/travis/pypa/virtualenv/develop.svg + :target: http://travis-ci.org/pypa/virtualenv + +* `Installation `_ +* `Documentation `_ +* `Changelog `_ +* `Issues `_ +* `PyPI `_ +* `Github `_ +* `User mailing list `_ +* `Dev mailing list `_ +* User IRC: #pypa on Freenode. +* Dev IRC: #pypa-dev on Freenode. + + +Code of Conduct +--------------- + +Everyone interacting in the virtualenv project's codebases, issue trackers, +chat rooms, and mailing lists is expected to follow the +`PyPA Code of Conduct`_. + +.. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/ diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..0d7ed7a45 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,25 @@ +environment: + + matrix: + # For Python versions available on Appveyor, see + # http://www.appveyor.com/docs/installed-software#python + + - PYTHON: "C:\\Python26" + - PYTHON: "C:\\Python27" + - PYTHON: "C:\\Python33" + - PYTHON: "C:\\Python34" + - PYTHON: "C:\\Python35" + - PYTHON: "C:\\Python26-x64" + - PYTHON: "C:\\Python27-x64" + - PYTHON: "C:\\Python33-x64" + - PYTHON: "C:\\Python34-x64" + - PYTHON: "C:\\Python35-x64" + +install: + # We need tox installed for the tests + - "%PYTHON%\\Scripts\\pip.exe install tox" + +build: off + +test_script: + - "%PYTHON%\\Scripts\\tox.exe -e py" diff --git a/bin/rebuild-script.py b/bin/rebuild-script.py new file mode 100755 index 000000000..d0bc93e5a --- /dev/null +++ b/bin/rebuild-script.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +""" +Helper script to rebuild virtualenv.py from virtualenv_support +""" +from __future__ import print_function + +import os +import re +import codecs +from zlib import crc32 as _crc32 + + +def crc32(data): + """Python version idempotent""" + return _crc32(data) & 0xffffffff + + +here = os.path.dirname(__file__) +script = os.path.join(here, '..', 'virtualenv.py') + +gzip = codecs.lookup('zlib') +b64 = codecs.lookup('base64') + +file_regex = re.compile( + br'##file (.*?)\n([a-zA-Z][a-zA-Z0-9_]+)\s*=\s*convert\("""\n(.*?)"""\)', + re.S) +file_template = b'##file %(filename)s\n%(varname)s = convert("""\n%(data)s""")' + + +def rebuild(script_path): + with open(script_path, 'rb') as f: + script_content = f.read() + parts = [] + last_pos = 0 + match = None + for match in file_regex.finditer(script_content): + parts += [script_content[last_pos:match.start()]] + last_pos = match.end() + filename, fn_decoded = match.group(1), match.group(1).decode() + varname = match.group(2) + data = match.group(3) + + print('Found file %s' % fn_decoded) + pathname = os.path.join(here, '..', 'virtualenv_embedded', fn_decoded) + + with open(pathname, 'rb') as f: + embedded = f.read() + new_crc = crc32(embedded) + new_data = b64.encode(gzip.encode(embedded)[0])[0] + + if new_data == data: + print(' File up to date (crc: %08x)' % new_crc) + parts += [match.group(0)] + continue + # Else: content has changed + crc = crc32(gzip.decode(b64.decode(data)[0])[0]) + print(' Content changed (crc: %08x -> %08x)' % + (crc, new_crc)) + new_match = file_template % { + b'filename': filename, + b'varname': varname, + b'data': new_data + } + parts += [new_match] + + parts += [script_content[last_pos:]] + new_content = b''.join(parts) + + if new_content != script_content: + print('Content updated; overwriting... ', end='') + with open(script_path, 'wb') as f: + f.write(new_content) + print('done.') + else: + print('No changes in content') + if match is None: + print('No variables were matched/found') + +if __name__ == '__main__': + rebuild(script) diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 000000000..e4de9f847 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,130 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-compressor.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-compressor.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/django-compressor" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-compressor" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + make -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/_static/custom.css b/docs/_static/custom.css deleted file mode 100644 index 689337387..000000000 --- a/docs/_static/custom.css +++ /dev/null @@ -1,65 +0,0 @@ -.wy-nav-content { - padding: 1em; -} - -#virtualenv img { - margin-bottom: 6px; -} - -/* Allow table content to wrap around */ -.wy-table-responsive table th, -.wy-table-responsive table td { - /* !important because RTD has conflicting stylesheets */ - white-space: normal !important; - padding: 8px 6px !important; -} - -.wy-table-responsive table { - width: 100%; - margin-left: 0 !important; -} - -.rst-content table.docutils td ol { - margin-bottom: 0; -} - -.rst-content table.docutils td ul { - margin-bottom: 0; -} - -.rst-content table.docutils td p { - margin-bottom: 0; -} - -div[class*="highlight-"] { - margin-bottom: 12px; -} - -/* Tweak whitespace on the release history page */ -#release-history p { - margin-bottom: 0; - margin-top: 0; -} - -#release-history h3 { - margin-bottom: 6px; -} - -#release-history ul { - margin-bottom: 12px; -} - -#release-history ul ul { - margin-bottom: 0; - margin-top: 0; -} - -#release-history h2 { - margin-bottom: 12px; -} - -/* Reduce whitespace on the inline-code snippets and add softer corners */ -.rst-content code { - padding: 2px 3px; - border-radius: 3px; -} diff --git a/docs/_static/rtd-search.js b/docs/_static/rtd-search.js deleted file mode 100644 index 133e1e31c..000000000 --- a/docs/_static/rtd-search.js +++ /dev/null @@ -1,11 +0,0 @@ -document.addEventListener("DOMContentLoaded", () => { - const searchInput = document.querySelector( - ".sidebar-search input[type='search']", - ); - if (searchInput) { - searchInput.addEventListener("focus", () => { - document.dispatchEvent(new CustomEvent("readthedocs-search-show")); - searchInput.blur(); - }); - } -}); diff --git a/docs/_static/virtualenv.png b/docs/_static/virtualenv.png deleted file mode 100644 index 1f729837d..000000000 Binary files a/docs/_static/virtualenv.png and /dev/null differ diff --git a/docs/_static/virtualenv.svg b/docs/_static/virtualenv.svg deleted file mode 100644 index 090396002..000000000 --- a/docs/_static/virtualenv.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - virtualenv - - - - - - - - diff --git a/docs/changelog.rst b/docs/changelog.rst deleted file mode 100644 index 67737dbab..000000000 --- a/docs/changelog.rst +++ /dev/null @@ -1,2189 +0,0 @@ -################# - Release History -################# - -.. towncrier-draft-entries:: [UNRELEASED DRAFT] - -.. towncrier release notes start - -********************** - v21.2.0 (2026-03-09) -********************** - -Features - 21.2.0 -================= - -- Update embed wheel generator (``tasks/upgrade_wheels.py``) to include type annotations in generated output - by - :user:`rahuldevikar`. (:issue:`3075`) - -Bugfixes - 21.2.0 -================= - -- Pass ``--without-scm-ignore-files`` to subprocess venv on Python 3.13+ so virtualenv controls ``.gitignore`` creation, - fixing flaky ``test_create_no_seed`` and ``--no-vcs-ignore`` being ignored in subprocess path - by - :user:`gaborbernat`. (:issue:`3089`) -- Use ``BASH_SOURCE[0]`` instead of ``$0`` in the bash activate script relocation fallback, fixing incorrect ``PATH`` - when sourcing the activate script from a different directory - by :user:`gaborbernat`. (:issue:`3090`) - -********************** - v21.1.0 (2026-02-27) -********************** - -Features - 21.1.0 -================= - -- Add comprehensive type annotations across the entire codebase and ship a PEP 561 ``py.typed`` marker so downstream - consumers and type checkers recognize virtualenv as an inline-typed package - by :user:`rahuldevikar`. (:issue:`3075`) - -********************** - v21.0.0 (2026-02-25) -********************** - -Deprecations and Removals - 21.0.0 -================================== - -- The Python discovery logic has been extracted into a standalone ``python-discovery`` package on PyPI (`documentation - `_) and is now consumed as a dependency. If you previously imported - discovery internals directly (e.g. ``from virtualenv.discovery.py_info import PythonInfo``), switch to ``from - python_discovery import PythonInfo``. Backward-compatibility re-export shims are provided at - ``virtualenv.discovery.py_info``, ``virtualenv.discovery.py_spec``, and ``virtualenv.discovery.cached_py_info``, - however these are considered unsupported and may be removed in a future release - by :user:`gaborbernat`. - (:issue:`3070`) - -*********************** - v20.39.1 (2026-02-25) -*********************** - -Features - 20.39.1 -================== - -- Add support for creating virtual environments with RustPython - by :user:`elmjag`. (:issue:`3010`) - -*********************** - v20.39.0 (2026-02-23) -*********************** - -Features - 20.39.0 -================== - -- Automatically resolve version manager shims (pyenv, mise, asdf) to the real Python binary during discovery, preventing - incorrect interpreter selection when shims are on ``PATH`` - by :user:`gaborbernat`. (:issue:`3049`) -- Add architecture (ISA) awareness to Python discovery โ€” users can now specify a CPU architecture suffix in the - ``--python`` spec string (e.g. ``cpython3.12-64-arm64``) to distinguish between interpreters that share the same - version and bitness but target different architectures. Uses ``sysconfig.get_platform()`` as the data source, with - cross-platform normalization (``amd64`` โ†” ``x86_64``, ``aarch64`` โ†” ``arm64``). Omitting the suffix preserves existing - behavior - by :user:`rahuldevikar`. (:issue:`3059`) - -*********************** - v20.38.0 (2026-02-19) -*********************** - -Features - 20.38.0 -================== - -- Store app data (pip/setuptools/wheel caches) under the OS cache directory (``platformdirs.user_cache_dir``) instead of - the data directory (``platformdirs.user_data_dir``). Existing app data at the old location is automatically migrated - on first use. This ensures cached files that can be redownloaded are placed in the standard cache location (e.g. - ``~/.cache`` on Linux, ``~/Library/Caches`` on macOS) where they are excluded from backups and can be cleaned by - system tools - by :user:`rahuldevikar`. (:issue:`1884`) (:issue:`1884`) -- Add ``PKG_CONFIG_PATH`` environment variable support to all activation scripts (Bash, Batch, PowerShell, Fish, C - Shell, Nushell, and Python). The virtualenv's ``lib/pkgconfig`` directory is now automatically prepended to - ``PKG_CONFIG_PATH`` on activation and restored on deactivation, enabling packages that use ``pkg-config`` during - build/install to find their configuration files - by :user:`rahuldevikar`. (:issue:`2637`) -- Upgrade embedded pip to ``26.0.1`` from ``25.3`` and setuptools to ``82.0.0``, ``75.3.4`` from ``75.3.2``, ``80.9.0`` - - by :user:`rahuldevikar`. (:issue:`3027`) -- Replace ``ty: ignore`` comments with proper type narrowing using assertions and explicit None checks - by - :user:`rahuldevikar`. (:issue:`3029`) - -Bugfixes - 20.38.0 -================== - -- Exclude pywin32 DLLs (``pywintypes*.dll``, ``pythoncom*.dll``) from being copied to the Scripts directory during - virtualenv creation on Windows. This fixes compatibility issues with pywin32, which expects its DLLs to be installed - in ``site-packages/pywin32_system32`` by its own post-install script - by :user:`rahuldevikar`. (:issue:`2662`) -- Preserve symlinks in ``pyvenv.cfg`` paths to match ``venv`` behavior. Use ``os.path.abspath()`` instead of - ``os.path.realpath()`` to normalize paths without resolving symlinks, fixing issues with Python installations accessed - via symlinked directories (common in network-mounted filesystems) - by :user:`rahuldevikar`. Fixes :issue:`2770`. - (:issue:`2770`) -- Fix Windows activation scripts to properly quote ``python.exe`` path, preventing failures when Python is installed in - a path with spaces (e.g., ``C:\Program Files``) and a file named ``C:\Program`` exists on the filesystem - by - :user:`rahuldevikar`. (:issue:`2985`) -- Fix ``bash -u`` (``set -o nounset``) compatibility in bash activation script by using ``${PKG_CONFIG_PATH:-}`` and - ``${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}`` to handle unset ``PKG_CONFIG_PATH`` - by :user:`Fridayai700`. - (:issue:`3044`) -- Gracefully handle corrupted on-disk cache and invalid JSON from Python interrogation subprocess instead of crashing - with unhandled ``JSONDecodeError`` or ``KeyError`` - by :user:`gaborbernat`. (:issue:`3054`) - -*********************** - v20.36.1 (2026-01-09) -*********************** - -Bugfixes - 20.36.1 -================== - -- Fix TOCTOU vulnerabilities in app_data and lock directory creation that could be exploited via symlink attacks - - reported by :user:`tsigouris007`, fixed by :user:`gaborbernat`. (:issue:`3013`) - -*********************** - v20.36.0 (2026-01-07) -*********************** - -Features - 20.36.0 -================== - -- Add support for PEP 440 version specifiers in the ``--python`` flag. Users can now specify Python versions using - operators like ``>=``, ``<=``, ``~=``, etc. For example: ``virtualenv --python=">=3.12" myenv`` `. (:issue:`2994`) - -*********************** - v20.35.4 (2025-10-28) -*********************** - -Bugfixes - 20.35.4 -================== - -- Fix race condition in ``_virtualenv.py`` when file is overwritten during import, preventing ``NameError`` when - ``_DISTUTILS_PATCH`` is accessed - by :user:`gracetyy`. (:issue:`2969`) -- Upgrade embedded wheels: - - - pip to ``25.3`` from ``25.2`` (:issue:`2989`) - -*********************** - v20.35.3 (2025-10-10) -*********************** - -Bugfixes - 20.35.3 -================== - -- Accept RuntimeError in `test_too_many_open_files`, by :user:`esafak` (:issue:`2935`) - -*********************** - v20.35.2 (2025-10-10) -*********************** - -Bugfixes - 20.35.2 -================== - -- Revert out changes related to the extraction of the discovery module - by :user:`gaborbernat`. (:issue:`2978`) - -*********************** - v20.35.1 (2025-10-09) -*********************** - -Bugfixes - 20.35.1 -================== - -- Patch get_interpreter to handle missing cache and app_data - by :user:`esafak` (:issue:`2972`) -- Fix backwards incompatible changes to ``PythonInfo`` - by :user:`gaborbernat`. (:issue:`2975`) - -*********************** - v20.35.0 (2025-10-08) -*********************** - -Features - 20.35.0 -================== - -- Add AppData and Cache protocols to discovery for decoupling - by :user:`esafak`. (:issue:`2074`) -- Ensure python3.exe and python3 on Windows for Python 3 - by :user:`esafak`. (:issue:`2774`) - -Bugfixes - 20.35.0 -================== - -- Replaced direct references to tcl/tk library paths with getattr - by :user:`esafak` (:issue:`2944`) -- Restore absolute import of fs_is_case_sensitive - by :user:`esafak`. (:issue:`2955`) - -*********************** - v20.34.0 (2025-08-13) -*********************** - -Features - 20.34.0 -================== - -- Abstract out caching in discovery - by :user:`esafak`. Decouple `FileCache` from `py_info` (discovery) - by - :user:`esafak`. Remove references to py_info in FileCache - by :user:`esafak`. Decouple discovery from creator plugins - - by :user:`esafak`. Decouple discovery by duplicating info utils - by :user:`esafak`. (:issue:`2074`) -- Add PyPy 3.11 support. Contributed by :user:`esafak`. (:issue:`2932`) - -Bugfixes - 20.34.0 -================== - -- Upgrade embedded wheel pip to ``25.2`` from ``25.1.1`` - by :user:`gaborbernat`. (:issue:`2333`) -- Accept RuntimeError in `test_too_many_open_files`, by :user:`esafak` (:issue:`2935`) -- Python in PATH takes precedence over uv-managed python. Contributed by :user:`edgarrmondragon`. (:issue:`2952`) - -*********************** - v20.33.1 (2025-08-05) -*********************** - -Bugfixes - 20.33.1 -================== - -- Correctly unpack _get_tcl_tk_libs() response in PythonInfo. Contributed by :user:`esafak`. (:issue:`2930`) -- Restore `py_info.py` timestamp in `test_py_info_cache_invalidation_on_py_info_change` Contributed by :user:`esafak`. - (:issue:`2933`) - -*********************** - v20.33.0 (2025-08-03) -*********************** - -Features - 20.33.0 -================== - -- Added support for Tcl and Tkinter. You're welcome. Contributed by :user:`esafak`. (:issue:`425`) - -Bugfixes - 20.33.0 -================== - -- Prevent logging setup when --help is passed, fixing a flaky test. Contributed by :user:`esafak`. (:issue:`u`) -- Fix cache invalidation for PythonInfo by hashing `py_info.py`. Contributed by :user:`esafak`. (:issue:`2467`) -- When no discovery plugins are found, the application would crash with a StopIteration. This change catches the - StopIteration and raises a RuntimeError with a more informative message. Contributed by :user:`esafak`. - (:issue:`2493`) -- Stop `--try-first-with` overriding absolute `--python` paths. Contributed by :user:`esafak`. (:issue:`2659`) -- Force UTF-8 encoding for pip download Contributed by :user:`esafak`. (:issue:`2780`) -- Creating a virtual environment on a filesystem without symlink-support would fail even with `--copies` Make - `fs_supports_symlink` perform a real symlink creation check on all platforms. Contributed by :user:`esafak`. - (:issue:`2786`) -- Add a note to the user guide recommending the use of a specific Python version when creating virtual environments. - Contributed by :user:`esafak`. (:issue:`2808`) -- Fix 'Too many open files' error due to a file descriptor leak in virtualenv's locking mechanism. Contributed by - :user:`esafak`. (:issue:`2834`) -- Support renamed Windows venv redirector (`venvlauncher.exe` and `venvwlauncher.exe`) on Python 3.13 Contributed by - :user:`esafak`. (:issue:`2851`) -- Resolve Nushell activation script deprecation warnings by dynamically selecting the ``--optional`` flag for Nushell - ``get`` command on version 0.106.0 and newer, while retaining the deprecated ``-i`` flag for older versions to - maintain compatibility. Contributed by :user:`gaborbernat`. (:issue:`2910`) - -*********************** - v20.32.0 (2025-07-20) -*********************** - -Features - 20.32.0 -================== - -- Warn on incorrect invocation of Nushell activation script - by :user:`esafak`. (:issue:`nushell_activation`) -- Discover uv-managed Python installations (:issue:`2901`) - -Bugfixes - 20.32.0 -================== - -- Ignore missing absolute paths for python discovery - by :user:`esafak` (:issue:`2870`) -- Upgrade embedded setuptools to ``80.9.0`` from ``80.3.1`` - by :user:`gaborbernat`. (:issue:`2900`) - -*********************** - v20.31.2 (2025-05-08) -*********************** - -No significant changes. - -*********************** - v20.31.1 (2025-05-05) -*********************** - -Bugfixes - 20.31.1 -================== - -- Upgrade embedded wheels: - - - pip to ``25.1.1`` from ``25.1`` - - setuptools to ``80.3.1`` from ``78.1.0`` (:issue:`2880`) - -*********************** - v20.31.0 (2025-05-05) -*********************** - -Features - 20.31.0 -================== - -- No longer bundle ``wheel`` wheels (except on Python 3.8), ``setuptools`` includes native ``bdist_wheel`` support. - Update ``pip`` to ``25.1``. (:issue:`2868`) - -Bugfixes - 20.31.0 -================== - -- ``get_embed_wheel()`` no longer fails with a :exc:`TypeError` when it is called with an unknown *distribution*. - (:issue:`2877`) -- Fix ``HelpFormatter`` error with Python 3.14.0b1. (:issue:`2878`) - -*********************** - v20.30.0 (2025-03-31) -*********************** - -Features - 20.30.0 -================== - -- Add support for `GraalPy `_. (:issue:`2832`) - -Bugfixes - 20.30.0 -================== - -- Upgrade embedded wheels: - - - setuptools to ``78.1.0`` from ``75.3.2`` (:issue:`2863`) - -*********************** - v20.29.3 (2025-03-06) -*********************** - -Bugfixes - 20.29.3 -================== - -- Ignore unreadable directories in ``PATH``. (:issue:`2794`) - -*********************** - v20.29.2 (2025-02-10) -*********************** - -Bugfixes - 20.29.2 -================== - -- Remove old virtualenv wheel from the source distribution - by :user:`gaborbernat`. (:issue:`2841`) -- Upgrade embedded wheel pip to ``25.0.1`` from ``24.3.1`` - by :user:`gaborbernat`. (:issue:`2843`) - -*********************** - v20.29.1 (2025-01-17) -*********************** - -Bugfixes - 20.29.1 -================== - -- Fix PyInfo cache incompatibility warnings - by :user:`robsdedude`. (:issue:`2827`) - -*********************** - v20.29.0 (2025-01-15) -*********************** - -Features - 20.29.0 -================== - -- Add support for selecting free-threaded Python interpreters, e.g., `python3.13t`. (:issue:`2809`) - -Bugfixes - 20.29.0 -================== - -- Upgrade embedded wheels: - - - setuptools to ``75.8.0`` from ``75.6.0`` (:issue:`2823`) - -*********************** - v20.28.1 (2025-01-02) -*********************** - -Bugfixes - 20.28.1 -================== - -- Skip tcsh tests on broken tcsh versions - by :user:`gaborbernat`. (:issue:`2814`) - -*********************** - v20.28.0 (2024-11-25) -*********************** - -Features - 20.28.0 -================== - -- Write CACHEDIR.TAG file on creation - by "user:`neilramsay`. (:issue:`2803`) - -*********************** - v20.27.2 (2024-11-25) -*********************** - -Bugfixes - 20.27.2 -================== - -- Upgrade embedded wheels: - - - setuptools to ``75.3.0`` from ``75.2.0`` (:issue:`2798`) - -- Upgrade embedded wheels: - - - wheel to ``0.45.0`` from ``0.44.0`` - - setuptools to ``75.5.0`` (:issue:`2800`) - -- no longer forcibly echo off during windows batch activation (:issue:`2801`) -- Upgrade embedded wheels: - - - setuptools to ``75.6.0`` from ``75.5.0`` - - wheel to ``0.45.1`` from ``0.45.0`` (:issue:`2804`) - -*********************** - v20.27.1 (2024-10-28) -*********************** - -Bugfixes - 20.27.1 -================== - -- Upgrade embedded wheels: - - - pip to ``24.3.1`` from ``24.2`` (:issue:`2789`) - -*********************** - v20.27.0 (2024-10-17) -*********************** - -Features - 20.27.0 -================== - -- Drop 3.7 support as the CI environments no longer allow it running - by :user:`gaborbernat`. (:issue:`2758`) - -Bugfixes - 20.27.0 -================== - -- When a ``$PATH`` entry cannot be checked for existence, skip it instead of terminating - by :user:`hroncok`. - (:issue:`2782`) -- Upgrade embedded wheels: - - - setuptools to ``75.2.0`` from ``75.1.0`` - - Removed pip of ``24.0`` - - Removed setuptools of ``68.0.0`` - - Removed wheel of ``0.42.0`` - - by :user:`gaborbernat`. (:issue:`2783`) - -- Fix zipapp is broken on Windows post distlib ``0.3.9`` - by :user:`gaborbernat`. (:issue:`2784`) - -*********************** - v20.26.6 (2024-09-27) -*********************** - -Bugfixes - 20.26.6 -================== - -- Properly quote string placeholders in activation script templates to mitigate potential command injection - by - :user:`y5c4l3`. (:issue:`2768`) - -*********************** - v20.26.5 (2024-09-17) -*********************** - -Bugfixes - 20.26.5 -================== - -- Upgrade embedded wheels: setuptools to ``75.1.0`` from ``74.1.2`` - by :user:`gaborbernat`. (:issue:`2765`) - -*********************** - v20.26.4 (2024-09-07) -*********************** - -Bugfixes - 20.26.4 -================== - -- no longer create `()` output in console during activation of a virtualenv by .bat file. (:issue:`2728`) -- Upgrade embedded wheels: - - - wheel to ``0.44.0`` from ``0.43.0`` - - pip to ``24.2`` from ``24.1`` - - setuptools to ``74.1.2`` from ``70.1.0`` (:issue:`2760`) - -*********************** - v20.26.3 (2024-06-21) -*********************** - -Bugfixes - 20.26.3 -================== - -- Upgrade embedded wheels: - - - setuptools to ``70.1.0`` from ``69.5.1`` - - pip to ``24.1`` from ``24.0`` (:issue:`2741`) - -*********************** - v20.26.2 (2024-05-13) -*********************** - -Bugfixes - 20.26.2 -================== - -- ``virtualenv.pyz`` no longer fails when zipapp path contains a symlink - by :user:`HandSonic` and :user:`petamas`. - (:issue:`1949`) -- Fix bad return code from activate.sh if hashing is disabled - by :user:'fenkes-ibm'. (:issue:`2717`) - -*********************** - v20.26.1 (2024-04-29) -*********************** - -Bugfixes - 20.26.1 -================== - -- fix PATH-based Python discovery on Windows - by :user:`ofek`. (:issue:`2712`) - -*********************** - v20.26.0 (2024-04-23) -*********************** - -Bugfixes - 20.26.0 -================== - -- allow builtin discovery to discover specific interpreters (e.g. ``python3.12``) given an unspecific spec (e.g. - ``python3``) - by :user:`flying-sheep`. (:issue:`2709`) - -*********************** - v20.25.3 (2024-04-17) -*********************** - -Bugfixes - 20.25.3 -================== - -- Python 3.13.0a6 renamed pathmod to parser. (:issue:`2702`) - -*********************** - v20.25.2 (2024-04-16) -*********************** - -Bugfixes - 20.25.2 -================== - -- Upgrade embedded wheels: - - - setuptools of ``69.1.0`` to ``69.5.1`` - - wheel of ``0.42.0`` to ``0.43.0`` (:issue:`2699`) - -*********************** - v20.25.1 (2024-02-21) -*********************** - -Bugfixes - 20.25.1 -================== - -- Upgrade embedded wheels: - - - setuptools to ``69.0.3`` from ``69.0.2`` - - pip to ``23.3.2`` from ``23.3.1`` (:issue:`2681`) - -- Upgrade embedded wheels: - - - pip ``23.3.2`` to ``24.0``, - - setuptools ``69.0.3`` to ``69.1.0``. (:issue:`2691`) - -Misc - 20.25.1 -============== - -- :issue:`2688` - -*********************** - v20.25.0 (2023-12-01) -*********************** - -Features - 20.25.0 -================== - -- The tests now pass on the CI with Python 3.13.0a2 - by :user:`hroncok`. (:issue:`2673`) - -Bugfixes - 20.25.0 -================== - -- Upgrade embedded wheels: - - - wheel to ``0.41.3`` from ``0.41.2`` (:issue:`2665`) - -- Upgrade embedded wheels: - - - wheel to ``0.42.0`` from ``0.41.3`` - - setuptools to ``69.0.2`` from ``68.2.2`` (:issue:`2669`) - -*********************** - v20.24.6 (2023-10-23) -*********************** - -Bugfixes - 20.24.6 -================== - -- Use get_hookimpls method instead of the private attribute in tests. (:issue:`2649`) -- Upgrade embedded wheels: - - - setuptools to ``68.2.2`` from ``68.2.0`` - - pip to ``23.3.1`` from ``23.2.1`` (:issue:`2656`) - -*********************** - v20.24.5 (2023-09-08) -*********************** - -Bugfixes - 20.24.5 -================== - -- Declare PyPy 3.10 support - by :user:`cclauss`. (:issue:`2638`) -- Brew on macOS no longer allows copy builds - disallow choosing this by :user:`gaborbernat`. (:issue:`2640`) -- Upgrade embedded wheels: - - - setuptools to ``68.2.0`` from ``68.1.2`` (:issue:`2642`) - -*********************** - v20.24.4 (2023-08-30) -*********************** - -Bugfixes - 20.24.4 -================== - -- Upgrade embedded wheels: - - - setuptools to ``68.1.2`` from ``68.1.0`` on ``3.8+`` - - wheel to ``0.41.2`` from ``0.41.1`` on ``3.7+`` (:issue:`2628`) - -*********************** - v20.24.3 (2023-08-11) -*********************** - -Bugfixes - 20.24.3 -================== - -- Fixed ResourceWarning on exit caused by periodic update subprocess (:issue:`2472`) -- Upgrade embedded wheels: - - - wheel to ``0.41.1`` from ``0.41.0`` (:issue:`2622`) - -Misc - 20.24.3 -============== - -- :issue:`2610` - -*********************** - v20.24.2 (2023-07-24) -*********************** - -Bugfixes - 20.24.2 -================== - -- Upgrade embedded wheels: - - - pip to ``23.2.1`` from ``23.2`` - - wheel to ``0.41.0`` from ``0.40.0`` (:issue:`2614`) - -*********************** - v20.24.1 (2023-07-19) -*********************** - -Bugfixes - 20.24.1 -================== - -- Upgrade embedded wheels: - - - pip to ``23.2`` from ``23.1.2`` - by :user:`arielkirkwood` (:issue:`2611`) - -*********************** - v20.24.0 (2023-07-14) -*********************** - -Features - 20.24.0 -================== - -- Export the prompt prefix as ``VIRTUAL_ENV_PROMPT`` when activating a virtual environment - by :user:`jimporter`. - (:issue:`2194`) - -Bugfixes - 20.24.0 -================== - -- Fix test suite - by :user:`gaborbernat`. (:issue:`2592`) -- Upgrade embedded wheels: - - - setuptools to ``68.0.0`` from ``67.8.0`` (:issue:`2607`) - -*********************** - v20.23.1 (2023-06-16) -*********************** - -Bugfixes - 20.23.1 -================== - -- update and simplify nushell activation script, fixes an issue on Windows resulting in consecutive command not found - - by :user:`melMass`. (:issue:`2572`) -- Upgrade embedded wheels: - - - setuptools to ``67.8.0`` from ``67.7.2`` (:issue:`2588`) - -*********************** - v20.23.0 (2023-04-27) -*********************** - -Features - 20.23.0 -================== - -- Do not install ``wheel`` and ``setuptools`` seed packages for Python 3.12+. To restore the old behavior use: - - - for ``wheel`` use ``VIRTUALENV_WHEEL=bundle`` environment variable or ``--wheel=bundle`` CLI flag, - - for ``setuptools`` use ``VIRTUALENV_SETUPTOOLS=bundle`` environment variable or ``--setuptools=bundle`` CLI flag. - - By :user:`chrysle`. (:issue:`2487`) - -- 3.12 support - by :user:`gaborbernat`. (:issue:`2558`) - -Bugfixes - 20.23.0 -================== - -- Prevent ``PermissionError`` when using venv creator on systems that deliver files without user write permission - by - :user:`kulikjak`. (:issue:`2543`) -- Upgrade setuptools to ``67.7.2`` from ``67.6.1`` and pip to ``23.1.2`` from ``23.1`` - by :user:`szleb`. - (:issue:`2560`) - -*********************** - v20.22.0 (2023-04-19) -*********************** - -Features - 20.22.0 -================== - -- Drop support for creating Python <=3.6 (including 2) interpreters. Removed pip of ``20.3.4``, ``21.3.1``; wheel of - ``0.37.1``; setuptools of ``59.6.0``, ``44.1.1``, ``50.3.2``- by :user:`gaborbernat`. (:issue:`2548`) - -*********************** - v20.21.1 (2023-04-19) -*********************** - -Bugfixes - 20.21.1 -================== - -- Add ``tox.ini`` to sdist - by :user:`mtelka`. (:issue:`2511`) -- Move the use of 'let' in nushell to ensure compatibility with future releases of nushell, where 'let' no longer - assumes that its initializer is a full expressions. (:issue:`2527`) -- The nushell command 'str collect' has been superseded by the 'str join' command. The activate.nu script has been - updated to reflect this change. (:issue:`2532`) -- Upgrade embedded wheels: - - - wheel to ``0.40.0`` from ``0.38.4`` - - setuptools to ``67.6.1`` from ``67.4.0`` - - pip to ``23.1`` from ``23.0.1`` (:issue:`2546`) - -*********************** - v20.21.0 (2023-03-12) -*********************** - -Features - 20.21.0 -================== - -- Make closure syntax explicitly starts with {||. (:issue:`2512`) - -Bugfixes - 20.21.0 -================== - -- Add ``print`` command to nushell print_prompt to ensure compatibility with future release of nushell, where - intermediate commands no longer print their result to stdout. (:issue:`2514`) -- Do not assume the default encoding. (:issue:`2515`) -- Make ``ReentrantFileLock`` thread-safe and, thereby, fix race condition in ``virtualenv.cli_run`` - by - :user:`radoering`. (:issue:`2516`) - -*********************** - v20.20.0 (2023-02-28) -*********************** - -Features - 20.20.0 -================== - -- Change environment variable existence check in Nushell activation script to not use deprecated command. - (:issue:`2506`) - -Bugfixes - 20.20.0 -================== - -- Discover CPython implementations distributed on Windows by any organization - by :user:`faph`. (:issue:`2504`) -- Upgrade embedded setuptools to ``67.4.0`` from ``67.1.0`` and pip to ``23.0.1`` from ``23.0`` - by - :user:`gaborbernat`. (:issue:`2510`) - -*********************** - v20.19.0 (2023-02-07) -*********************** - -Features - 20.19.0 -================== - -- Allow platformdirs version ``3`` - by :user:`cdce8p`. (:issue:`2499`) - -*********************** - v20.18.0 (2023-02-06) -*********************** - -Features - 20.18.0 -================== - -- Drop ``3.6`` runtime support (can still create ``2.7+``) - by :user:`gaborbernat`. (:issue:`2489`) - -Bugfixes - 20.18.0 -================== - -- Fix broken prompt in Nushell when activating virtual environment - by :user:`kubouc`. (:issue:`2481`) -- Bump embedded pip to ``23.0`` and setuptools to ``67.1`` - by :user:`gaborbernat`. (:issue:`2489`) - -*********************** - v20.17.1 (2022-12-05) -*********************** - -Bugfixes - 20.17.1 -================== - -- A ``py`` or ``python`` spec means any Python rather than ``CPython`` - by :user:`gaborbernat`. (`#2460 - `_) -- Make ``activate.nu`` respect ``VIRTUAL_ENV_DISABLE_PROMPT`` and not set the prompt if requested - by :user:`m-lima`. - (`#2461 `_) - -*********************** - v20.17.0 (2022-11-27) -*********************** - -Features - 20.17.0 -================== - -- Change Nushell activation script to be a module meant to be activated as an overlay. (`#2422 - `_) -- Update operator used in Nushell activation script to be compatible with future versions. (`#2450 - `_) - -Bugfixes - 20.17.0 -================== - -- Do not use deprecated API from ``importlib.resources`` on Python 3.10 or later - by :user:`gaborbernat`. (`#2448 - `_) -- Upgrade embedded setuptools to ``65.6.3`` from ``65.5.1`` - by :user:`gaborbernat`. (`#2451 - `_) - -*********************** - v20.16.7 (2022-11-12) -*********************** - -Bugfixes - 20.16.7 -================== - -- Use parent directory of python executable for pyvenv.cfg "home" value per PEP 405 - by :user:`vfazio`. (`#2440 - `_) -- In POSIX virtual environments, try alternate binary names if ``sys._base_executable`` does not exist - by - :user:`vfazio`. (`#2442 `_) -- Upgrade embedded wheel to ``0.38.4`` and pip to ``22.3.1`` from ``22.3`` and setuptools to ``65.5.1`` from ``65.5.0`` - - by :user:`gaborbernat`. (`#2443 `_) - -*********************** - v20.16.6 (2022-10-25) -*********************** - -Features - 20.16.6 -================== - -- Drop unneeded shims for PyPy3 directory structure (`#2426 `_) - -Bugfixes - 20.16.6 -================== - -- Fix selected scheme on debian derivatives for python 3.10 when ``python3-distutils`` is not installed or the ``venv`` - scheme is not available - by :user:`asottile`. (`#2350 `_) -- Allow the test suite to pass even with the original C shell (rather than ``tcsh``) - by :user:`kulikjak`. (`#2418 - `_) -- Fix fallback handling of downloading wheels for bundled packages - by :user:`schaap`. (`#2429 - `_) -- Upgrade embedded setuptools to ``65.5.0`` from ``65.3.0`` and pip to ``22.3`` from ``22.2.2`` - by - :user:`gaborbernat`. (`#2434 `_) - -*********************** - v20.16.5 (2022-09-07) -*********************** - -Bugfixes - 20.16.5 -================== - -- Do not turn echo off for subsequent commands in batch activators (``activate.bat`` and ``deactivate.bat``) - by - :user:`pawelszramowski`. (`#2411 `_) - -*********************** - v20.16.4 (2022-08-29) -*********************** - -Bugfixes - 20.16.4 -================== - -- Bump embed setuptools to ``65.3`` - by :user:`gaborbernat`. (`#2405 - `_) - -*********************** - v20.16.3 (2022-08-04) -*********************** - -Bugfixes - 20.16.3 -================== - -- Upgrade embedded pip to ``22.2.2`` from ``22.2.1`` and setuptools to ``63.4.1`` from ``63.2.0`` - by - :user:`gaborbernat`. (`#2395 `_) - -*********************** - v20.16.2 (2022-07-27) -*********************** - -Bugfixes - 20.16.2 -================== - -- Bump embedded pip from ``22.2`` to ``22.2.1`` - by :user:`gaborbernat`. (`#2391 - `_) - -*********************** - v20.16.1 (2022-07-26) -*********************** - -Features - 20.16.1 -================== - -- Update Nushell activation scripts to version 0.67 - by :user:`kubouch`. (`#2386 - `_) - -*********************** - v20.16.0 (2022-07-25) -*********************** - -Features - 20.16.0 -================== - -- Drop support for running under Python 2 (still can generate Python 2 environments) - by :user:`gaborbernat`. (`#2382 - `_) -- Upgrade embedded pip to ``22.2`` from ``22.1.2`` and setuptools to ``63.2.0`` from ``62.6.0`` - by - :user:`gaborbernat`. (`#2383 `_) - -*********************** - v20.15.1 (2022-06-28) -*********************** - -Bugfixes - 20.15.1 -================== - -- Fix the incorrect operation when ``setuptools`` plugins output something into ``stdout``. (`#2335 - `_) -- CPython3Windows creator ignores missing ``DLLs`` dir. (`#2368 `_) - -*********************** - v20.15.0 (2022-06-25) -*********************** - -Features - 20.15.0 -================== - -- Support for Windows embeddable Python package: includes ``python.zip`` in the creator sources - by - :user:`reksarka`. (`#1774 `_) - -Bugfixes - 20.15.0 -================== - -- Upgrade embedded setuptools to ``62.3.3`` from ``62.6.0`` and pip to ``22.1.2`` from ``22.0.4`` - by - :user:`gaborbernat`. (`#2348 `_) -- Use ``shlex.quote`` instead of deprecated ``pipes.quote`` in Python 3 - by :user:`frenzymadness`. (`#2351 - `_) -- Fix Windows PyPy 3.6 - by :user:`reksarka`. (`#2363 `_) - -*********************** - v20.14.1 (2022-04-11) -*********************** - -Features - 20.14.1 -================== - -- Support for creating a virtual environment from a Python 2.7 framework on macOS 12 - by :user:`nickhutchinson`. - (`#2284 `_) - -Bugfixes - 20.14.1 -================== - -- Upgrade embedded setuptools to ``62.1.0`` from ``61.0.0`` - by :user:`gaborbernat`. (`#2327 - `_) - -*********************** - v20.14.0 (2022-03-25) -*********************** - -Features - 20.14.0 -================== - -- Support Nushell activation scripts with nu version ``0.60`` - by :user:`kubouch`. (`#2321 - `_) - -Bugfixes - 20.14.0 -================== - -- Upgrade embedded setuptools to ``61.0.0`` from ``60.10.0`` - by :user:`gaborbernat`. (`#2322 - `_) - -*********************** - v20.13.4 (2022-03-18) -*********************** - -Bugfixes - 20.13.4 -================== - -- Improve performance of python startup inside created virtualenvs - by :user:`asottile`. (`#2317 - `_) -- Upgrade embedded setuptools to ``60.10.0`` from ``60.9.3`` - by :user:`gaborbernat`. (`#2320 - `_) - -*********************** - v20.13.3 (2022-03-07) -*********************** - -Bugfixes - 20.13.3 -================== - -- Avoid symlinking the contents of ``/usr`` into PyPy3.8+ virtualenvs - by :user:`stefanor`. (`#2310 - `_) -- Bump embed pip from ``22.0.3`` to ``22.0.4`` - by :user:`gaborbernat`. (`#2311 - `_) - -*********************** - v20.13.2 (2022-02-24) -*********************** - -Bugfixes - 20.13.2 -================== - -- Upgrade embedded setuptools to ``60.9.3`` from ``60.6.0`` - by :user:`gaborbernat`. (`#2306 - `_) - -*********************** - v20.13.1 (2022-02-05) -*********************** - -Bugfixes - 20.13.1 -================== - -- fix "execv() arg 2 must contain only strings" error on M1 MacOS (`#2282 - `_) -- Upgrade embedded setuptools to ``60.5.0`` from ``60.2.0`` - by :user:`asottile`. (`#2289 - `_) -- Upgrade embedded pip to ``22.0.3`` and setuptools to ``60.6.0`` - by :user:`gaborbernat` and :user:`asottile`. (`#2294 - `_) - -*********************** - v20.13.0 (2022-01-02) -*********************** - -Features - 20.13.0 -================== - -- Add downloaded wheel information in the relevant JSON embed file to prevent additional downloads of the same wheel. - - by :user:`mayeut`. (`#2268 `_) - -Bugfixes - 20.13.0 -================== - -- Fix ``AttributeError: 'bool' object has no attribute 'error'`` when creating a Python 2.x virtualenv on macOS - by - ``moreati``. (`#2269 `_) -- Fix ``PermissionError: [Errno 1] Operation not permitted`` when creating a Python 2.x virtualenv on macOS/arm64 - by - ``moreati``. (`#2271 `_) - -*********************** - v20.12.1 (2022-01-01) -*********************** - -Bugfixes - 20.12.1 -================== - -- Try using previous updates of ``pip``, ``setuptools`` & ``wheel`` when inside an update grace period rather than - always falling back to embedded wheels - by :user:`mayeut`. (`#2265 - `_) -- New patch versions of ``pip``, ``setuptools`` & ``wheel`` are now returned in the expected timeframe. - by - :user:`mayeut`. (`#2266 `_) -- Manual upgrades of ``pip``, ``setuptools`` & ``wheel`` are not discarded by a periodic update - by :user:`mayeut`. - (`#2267 `_) - -*********************** - v20.12.0 (2021-12-31) -*********************** - -Features - 20.12.0 -================== - -- Sign the python2 exe on Darwin arm64 - by :user:`tmspicer`. (`#2233 - `_) - -Bugfixes - 20.12.0 -================== - -- Fix ``--download`` option - by :user:`mayeut`. (`#2120 `_) -- Upgrade embedded setuptools to ``60.2.0`` from ``60.1.1`` - by :user:`gaborbernat`. (`#2263 - `_) - -*********************** - v20.11.2 (2021-12-29) -*********************** - -Bugfixes - 20.11.2 -================== - -- Fix installation of pinned versions of ``pip``, ``setuptools`` & ``wheel`` - by :user:`mayeut`. (`#2203 - `_) - -*********************** - v20.11.1 (2021-12-29) -*********************** - -Bugfixes - 20.11.1 -================== - -- Bump embed setuptools to ``60.1.1`` from ``60.1.0`` - by :user:`gaborbernat`. (`#2258 - `_) - -*********************** - v20.11.0 (2021-12-28) -*********************** - -Features - 20.11.0 -================== - -- Avoid deprecation warning from py-filelock argument - by :user:`ofek`. (`#2237 - `_) -- Upgrade embedded setuptools to ``61.1.0`` from ``58.3.0`` - by :user:`gaborbernat`. (`#2240 - `_) -- Drop the runtime dependency of ``backports.entry-points-selectable`` - by :user:`hroncok`. (`#2246 - `_) -- Fish: PATH variables should not be quoted when being set - by :user:`d3dave`. (`#2248 - `_) - -*********************** - v20.10.0 (2021-11-01) -*********************** - -Features - 20.10.0 -================== - -- If a ``"venv"`` install scheme exists in ``sysconfig``, virtualenv now uses it to create new virtual environments. - This allows Python distributors, such as Fedora, to patch/replace the default install scheme without affecting the - paths in new virtual environments. A similar technique `was proposed to Python, for the venv module - `_ - by ``hroncok`` (`#2208 `_) -- The activated virtualenv prompt is now always wrapped in parentheses. This affects venvs created with the ``--prompt`` - attribute, and matches virtualenv's behavior on par with venv. (`#2224 - `_) - -Bugfixes - 20.10.0 -================== - -- Fix broken prompt set up by activate.bat - by :user:`SiggyBar`. (`#2225 - `_) - -********************** - v20.9.0 (2021-10-23) -********************** - -Features - 20.9.0 -================= - -- Special-case ``--prompt .`` to the name of the current directory - by :user:`rkm`. (`#2220 - `_) -- Add libffi-8.dll to pypy windows `#2218 `_ - by :user:`mattip` - -Bugfixes - 20.9.0 -================= - -- Fixed path collision that could lead to a PermissionError or writing to system directories when using PyPy3.8 - by - :user:`mgorny`. (`#2182 `_) -- Upgrade embedded setuptools to ``58.3.0`` from ``58.1.0`` and pip to ``21.3.1`` from ``21.2.4`` - by - :user:`gaborbernat`. (`#2205 `_) -- Remove stray closing parenthesis in activate.bat - by :user:`SiggyBar`. (`#2221 - `_) - -********************** - v20.8.1 (2021-09-24) -********************** - -Bugfixes - 20.8.1 -================= - -- Fixed a bug where while creating a venv on top of an existing one, without cleaning, when seeded wheel version - mismatch occurred, multiple ``.dist-info`` directories may be present, confounding entrypoint discovery - by - :user:`arcivanov` (`#2185 `_) -- Bump embed setuptools from ``58.0.4`` to ``58.1.0`` - by :user:`gaborbernat`. (`#2195 - `_) - -Misc - 20.8.1 -============= - -- `#2189 `_ - -********************** - v20.8.0 (2021-09-16) -********************** - -- upgrade embedded setuptools to ``58.0.4`` from ``57.4.0`` and pip to ``21.2.4`` from ``21.2.3`` -- Add nushell activation script - -********************** - v20.7.2 (2021-08-10) -********************** - -Bugfixes - 20.7.2 -================= - -- Upgrade embedded pip to ``21.2.3`` from ``21.2.2`` and wheel to ``0.37.0`` from ``0.36.2`` - by :user:`gaborbernat`. - (`#2168 `_) - -********************** - v20.7.1 (2021-08-09) -********************** - -Bugfixes - 20.7.1 -================= - -- Fix unpacking dictionary items in PythonInfo.install_path (`#2165 `_) - -********************** - v20.7.0 (2021-07-31) -********************** - -Bugfixes - 20.7.0 -================= - -- upgrade embedded pip to ``21.2.2`` from ``21.1.3`` and setuptools to ``57.4.0`` from ``57.1.0`` - by - :user:`gaborbernat` (`#2159 `_) - -Deprecations and Removals - 20.7.0 -================================== - -- Removed ``xonsh`` activator due to this breaking fairly often the CI and lack of support from those packages - maintainers, upstream is encouraged to continue supporting the project as a `plugin - `_ - by :user:`gaborbernat`. (`#2160 - `_) - -********************** - v20.6.0 (2021-07-14) -********************** - -Features - 20.6.0 -================= - -- Support Python interpreters without ``distutils`` (fallback to ``syconfig`` in these cases) - by :user:`gaborbernat`. - (`#1910 `_) - -********************** - v20.5.0 (2021-07-13) -********************** - -Features - 20.5.0 -================= - -- Plugins now use 'selectable' entry points - by :user:`jaraco`. (`#2093 - `_) -- add libffi-7.dll to the hard-coded list of dlls for PyPy (`#2141 `_) -- Use the better maintained ``platformdirs`` instead of ``appdirs`` - by :user:`gaborbernat`. (`#2142 - `_) - -Bugfixes - 20.5.0 -================= - -- Bump pip the embedded pip ``21.1.3`` and setuptools to ``57.1.0`` - by :user:`gaborbernat`. (`#2135 - `_) - -Deprecations and Removals - 20.5.0 -================================== - -- Drop python ``3.4`` support as it has been over 2 years since EOL - by :user:`gaborbernat`. (`#2141 - `_) - -********************** - v20.4.7 (2021-05-24) -********************** - -Bugfixes - 20.4.7 -================= - -- Upgrade embedded pip to ``21.1.2`` and setuptools to ``57.0.0`` - by :user:`gaborbernat`. (`#2123 - `_) - -********************** - v20.4.6 (2021-05-05) -********************** - -Bugfixes - 20.4.6 -================= - -- Fix ``site.getsitepackages()`` broken on python2 on debian - by :user:`freundTech`. (`#2105 - `_) - -********************** - v20.4.5 (2021-05-05) -********************** - -Bugfixes - 20.4.5 -================= - -- Bump pip to ``21.1.1`` from ``21.0.1`` - by :user:`gaborbernat`. (`#2104 - `_) -- Fix ``site.getsitepackages()`` ignoring ``--system-site-packages`` on python2 - by :user:`freundTech`. (`#2106 - `_) - -********************** - v20.4.4 (2021-04-20) -********************** - -Bugfixes - 20.4.4 -================= - -- Built in discovery class is always preferred over plugin supplied classes. (`#2087 - `_) -- Upgrade embedded setuptools to ``56.0.0`` by :user:`gaborbernat`. (`#2094 - `_) - -********************** - v20.4.3 (2021-03-16) -********************** - -Bugfixes - 20.4.3 -================= - -- Bump embedded setuptools from ``52.0.0`` to ``54.1.2`` - by :user:`gaborbernat` (`#2069 - `_) -- Fix PyPy3 stdlib on Windows is incorrect - by :user:`gaborbernat`. (`#2071 - `_) - -********************** - v20.4.2 (2021-02-01) -********************** - -Bugfixes - 20.4.2 -================= - -- Running virtualenv ``--upgrade-embed-wheels`` crashes - by :user:`gaborbernat`. (`#2058 - `_) - -********************** - v20.4.1 (2021-01-31) -********************** - -Bugfixes - 20.4.1 -================= - -- Bump embedded pip and setuptools packages to latest upstream supported (``21.0.1`` and ``52.0.0``) - by - :user:`gaborbernat`. (`#2060 `_) - -********************** - v20.4.0 (2021-01-19) -********************** - -Features - 20.4.0 -================= - -- On the programmatic API allow passing in the environment variable dictionary to use, defaults to ``os.environ`` if not - specified - by :user:`gaborbernat`. (`#2054 `_) - -Bugfixes - 20.4.0 -================= - -- Upgrade embedded setuptools to ``51.3.3`` from ``51.1.2`` - by :user:`gaborbernat`. (`#2055 - `_) - -********************** - v20.3.1 (2021-01-13) -********************** - -Bugfixes - 20.3.1 -================= - -- Bump embed pip to ``20.3.3``, setuptools to ``51.1.1`` and wheel to ``0.36.2`` - by :user:`gaborbernat`. (`#2036 - `_) -- Allow unfunctioning of pydoc to fail freely so that virtualenvs can be activated under Zsh with set -e (since - otherwise ``unset -f`` and ``unfunction`` exit with 1 if the function does not exist in Zsh) - by :user:`d125q`. - (`#2049 `_) -- Drop cached python information if the system executable is no longer present (for example when the executable is a - shim and the mapped executable is replaced - such is the case with pyenv) - by :user:`gaborbernat`. (`#2050 - `_) - -********************** - v20.3.0 (2021-01-10) -********************** - -Features - 20.3.0 -================= - -- The builtin discovery takes now a ``--try-first-with`` argument and is first attempted as valid interpreters. One can - use this to force discovery of a given python executable when the discovery order/mechanism raises errors - by - :user:`gaborbernat`. (`#2046 `_) - -Bugfixes - 20.3.0 -================= - -- On Windows python ``3.7+`` distributions where the exe shim is missing fallback to the old ways - by - :user:`gaborbernat`. (`#1986 `_) -- When discovering interpreters on Windows, via the PEP-514, prefer ``PythonCore`` releases over other ones. virtualenv - is used via pip mostly by this distribution, so prefer it over other such as conda - by :user:`gaborbernat`. (`#2046 - `_) - -********************** - v20.2.2 (2020-12-07) -********************** - -Bugfixes - 20.2.2 -================= - -- Bump pip to ``20.3.1``, setuptools to ``51.0.0`` and wheel to ``0.36.1`` - by :user:`gaborbernat`. (`#2029 - `_) - -********************** - v20.2.1 (2020-11-23) -********************** - -No significant changes. - -********************** - v20.2.0 (2020-11-21) -********************** - -Features - 20.2.0 -================= - -- Optionally skip VCS ignore directive for entire virtualenv directory, using option :option:`no-vcs-ignore`, by default - ``False``. (`#2003 `_) -- Add ``--read-only-app-data`` option to allow for creation based on an existing app data cache which is non-writable. - This may be useful (for example) to produce a docker image where the app-data is pre-populated. - - .. code-block:: dockerfile - - ENV \ - VIRTUALENV_OVERRIDE_APP_DATA=/opt/virtualenv/cache \ - VIRTUALENV_SYMLINK_APP_DATA=1 - RUN virtualenv venv && rm -rf venv - ENV VIRTUALENV_READ_ONLY_APP_DATA=1 - USER nobody - # this virtualenv has symlinks into the read-only app-data cache - RUN virtualenv /tmp/venv - - Patch by :user:`asottile`. (`#2009 `_) - -Bugfixes - 20.2.0 -================= - -- Fix processing of the ``VIRTUALENV_PYTHON`` environment variable and make it multi-value as well (separated by comma) - - by :user:`pneff`. (`#1998 `_) - -********************** - v20.1.0 (2020-10-25) -********************** - -Features - 20.1.0 -================= - -- The python specification can now take one or more values, first found is used to create the virtual environment - by - :user:`gaborbernat`. (`#1995 `_) - -*********************** - v20.0.35 (2020-10-15) -*********************** - -Bugfixes - 20.0.35 -================== - -- Bump embedded setuptools from ``50.3.0`` to ``50.3.1`` - by :user:`gaborbernat`. (`#1982 - `_) -- After importing virtualenv passing cwd to a subprocess calls breaks with ``invalid directory`` - by - :user:`gaborbernat`. (`#1983 `_) - -*********************** - v20.0.34 (2020-10-12) -*********************** - -Bugfixes - 20.0.34 -================== - -- Align with venv module when creating virtual environments with builtin creator on Windows 3.7 and later - by - :user:`gaborbernat`. (`#1782 `_) -- Handle Cygwin path conversion in the activation script - by :user:`davidcoghlan`. (`#1969 - `_) - -*********************** - v20.0.33 (2020-10-04) -*********************** - -Bugfixes - 20.0.33 -================== - -- Fix ``None`` type error in cygwin if POSIX path in dest - by :user:`danyeaw`. (`#1962 - `_) -- Fix Python 3.4 incompatibilities (added back to the CI) - by :user:`gaborbernat`. (`#1963 - `_) - -*********************** - v20.0.32 (2020-10-01) -*********************** - -Bugfixes - 20.0.32 -================== - -- For activation scripts always use UNIX line endings (unless it's BATCH shell related) - by :user:`saytosid`. (`#1818 - `_) -- Upgrade embedded pip to ``20.2.1`` and setuptools to ``49.4.0`` - by :user:`gaborbernat`. (`#1918 - `_) -- Avoid spawning new windows when doing seed package upgrades in the background on Windows - by :user:`gaborbernat`. - (`#1928 `_) -- Fix a bug that reading and writing on the same file may cause race on multiple processes. (`#1938 - `_) -- Upgrade embedded setuptools to ``50.2.0`` and pip to ``20.2.3`` - by :user:`gaborbernat`. (`#1939 - `_) -- Provide correct path for bash activator in cygwin or msys2 - by :user:`danyeaw`. (`#1940 - `_) -- Relax importlib requirement to allow version<3 - by :user:`usamasadiq` (`#1953 - `_) -- pth files were not processed on CPython2 if $PYTHONPATH was pointing to site-packages/ - by :user:`navytux`. (`#1959 - `_) (`#1960 `_) - -*********************** - v20.0.31 (2020-08-17) -*********************** - -Bugfixes - 20.0.31 -================== - -- Upgrade embedded pip to ``20.2.1``, setuptools to ``49.6.0`` and wheel to ``0.35.1`` - by :user:`gaborbernat`. (`#1918 - `_) - -*********************** - v20.0.30 (2020-08-04) -*********************** - -Bugfixes - 20.0.30 -================== - -- Upgrade pip to ``20.2.1`` and setuptools to ``49.2.1`` - by :user:`gaborbernat`. (`#1915 - `_) - -*********************** - v20.0.29 (2020-07-31) -*********************** - -Bugfixes - 20.0.29 -================== - -- Upgrade embedded pip from version ``20.1.2`` to ``20.2`` - by :user:`gaborbernat`. (`#1909 - `_) - -*********************** - v20.0.28 (2020-07-24) -*********************** - -Bugfixes - 20.0.28 -================== - -- Fix test suite failing if run from system Python - by :user:`gaborbernat`. (`#1882 - `_) -- Provide ``setup_logging`` flag to python API so that users can bypass logging handling if their application already - performs this - by :user:`gaborbernat`. (`#1896 `_) -- Use ``\n`` instead if ``\r\n`` as line separator for report (because Python already performs this transformation - automatically upon write to the logging pipe) - by :user:`gaborbernat`. (`#1905 - `_) - -*********************** - v20.0.27 (2020-07-15) -*********************** - -Bugfixes - 20.0.27 -================== - -- No longer preimport threading to fix support for `gpython `_ and `gevent - `_ - by :user:`navytux`. (`#1897 `_) -- Upgrade setuptools from ``49.2.0`` on ``Python 3.5+`` - by :user:`gaborbernat`. (`#1898 - `_) - -*********************** - v20.0.26 (2020-07-07) -*********************** - -Bugfixes - 20.0.26 -================== - -- Bump dependency ``distutils >= 0.3.1`` - by :user:`gaborbernat`. (`#1880 - `_) -- Improve periodic update handling: - - - better logging output while running and enable logging on background process call ( - ``_VIRTUALENV_PERIODIC_UPDATE_INLINE`` may be used to debug behavior inline) - - fallback to unverified context when querying the PyPi for release date, - - stop downloading wheels once we reach the embedded version, - - by :user:`gaborbernat`. (`#1883 `_) - -- Do not print error message if the application exists with ``SystemExit(0)`` - by :user:`gaborbernat`. (`#1885 - `_) -- Upgrade embedded setuptools from ``47.3.1`` to ``49.1.0`` for Python ``3.5+`` - by :user:`gaborbernat`. (`#1887 - `_) - -*********************** - v20.0.25 (2020-06-23) -*********************** - -Bugfixes - 20.0.25 -================== - -- Fix that when the ``app-data`` seeders image creation fails the exception is silently ignored. Avoid two virtual - environment creations to step on each others toes by using a lock while creating the base images. By - :user:`gaborbernat`. (`#1869 `_) - -*********************** - v20.0.24 (2020-06-22) -*********************** - -Features - 20.0.24 -================== - -- Ensure that the seeded packages do not get too much out of date: - - - add a CLI flag that triggers upgrade of embedded wheels under :option:`upgrade-embed-wheels` - - periodically (once every 14 days) upgrade the embedded wheels in a background process, and use them if they have - been released for more than 28 days (can be disabled via :option:`no-periodic-update`) - - More details under :ref:`wheels` - by :user:`gaborbernat`. (`#1821 `_) - -- Upgrade embed wheel content: - - - ship wheels for Python ``3.9`` and ``3.10`` - - upgrade setuptools for Python ``3.5+`` from ``47.1.1`` to ``47.3.1`` - - by :user:`gaborbernat`. (`#1841 `_) - -- Display the installed seed package versions in the final summary output, for example: - - .. code-block:: console - - created virtual environment CPython3.8.3.final.0-64 in 350ms - creator CPython3Posix(dest=/x, clear=True, global=False) - seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/y/virtualenv) - added seed packages: pip==20.1.1, setuptools==47.3.1, wheel==0.34.2 - - by :user:`gaborbernat`. (`#1864 `_) - -Bugfixes - 20.0.24 -================== - -- Do not generate/overwrite ``.gitignore`` if it already exists at destination path - by :user:`gaborbernat`. (`#1862 - `_) -- Improve error message for no ``.dist-info`` inside the ``app-data`` copy seeder - by :user:`gaborbernat`. (`#1867 - `_) - -Improved Documentation - 20.0.24 -================================ - -- How seeding mechanisms discover (and automatically keep it up to date) wheels at :ref:`wheels` - by - :user:`gaborbernat`. (`#1821 `_) -- How distributions should handle shipping their own embedded wheels at :ref:`distribution_wheels` - by - :user:`gaborbernat`. (`#1840 `_) - -*********************** - v20.0.23 (2020-06-12) -*********************** - -Bugfixes - 20.0.23 -================== - -- Fix typo in ``setup.cfg`` - by :user:`RowdyHowell`. (`#1857 `_) - -*********************** - v20.0.22 (2020-06-12) -*********************** - -Bugfixes - 20.0.22 -================== - -- Relax ``importlib.resources`` requirement to also allow version 2 - by :user:`asottile`. (`#1846 - `_) -- Upgrade embedded setuptools to ``44.1.1`` for python 2 and ``47.1.1`` for python3.5+ - by :user:`gaborbernat`. (`#1855 - `_) - -*********************** - v20.0.21 (2020-05-20) -*********************** - -Features - 20.0.21 -================== - -- Generate ignore file for version control systems to avoid tracking virtual environments by default. Users should - remove these files if still want to track. For now we support only **git** by :user:`gaborbernat`. (`#1806 - `_) - -Bugfixes - 20.0.21 -================== - -- Fix virtualenv fails sometimes when run concurrently, ``--clear-app-data`` conflicts with :option:`clear` flag when - abbreviation is turned on. To bypass this while allowing abbreviated flags on the command line we had to move it to - :option:`reset-app-data` - by :user:`gaborbernat`. (`#1824 `_) -- Upgrade embedded ``setuptools`` to ``46.4.0`` from ``46.1.3`` on Python ``3.5+``, and ``pip`` from ``20.1`` to - ``20.1.1`` - by :user:`gaborbernat`. (`#1827 `_) -- Seeder pip now correctly handles ``--extra-search-dir`` - by :user:`frenzymadness`. (`#1834 - `_) - -*********************** - v20.0.20 (2020-05-04) -*********************** - -Bugfixes - 20.0.20 -================== - -- Fix download fails with python 3.4 - by :user:`gaborbernat`. (`#1809 - `_) -- Fixes older CPython2 versions use ``_get_makefile_filename`` instead of ``get_makefile_filename`` on ``sysconfig`` - - by :user:`ianw`. (`#1810 `_) -- Fix download is ``True`` by default - by :user:`gaborbernat`. (`#1813 - `_) -- Fail ``app-data`` seed operation when wheel download fails and better error message - by :user:`gaborbernat`. (`#1814 - `_) - -*********************** - v20.0.19 (2020-05-03) -*********************** - -Bugfixes - 20.0.19 -================== - -- Fix generating a Python 2 environment from Python 3 creates invalid python activator - by :user:`gaborbernat`. (`#1776 - `_) -- Fix pinning seed packages via ``app-data`` seeder raised ``Invalid Requirement`` - by :user:`gaborbernat`. (`#1779 - `_) -- Do not stop interpreter discovery if we fail to find the system interpreter for a executable during discovery - by - :user:`gaborbernat`. (`#1781 `_) -- On CPython2 POSIX platforms ensure ``syconfig.get_makefile_filename`` exists within the virtual environment (this is - used by some c-extension based libraries - e.g. numpy - for building) - by :user:`gaborbernat`. (`#1783 - `_) -- Better handling of options :option:`copies` and :option:`symlinks`. Introduce priority of where the option is set to - follow the order: CLI, env var, file, hardcoded. If both set at same level prefers copy over symlink. - by - :user:`gaborbernat`. (`#1784 `_) -- Upgrade pip for Python ``2.7`` and ``3.5+`` from ``20.0.2`` to ``20.1`` - by :user:`gaborbernat`. (`#1793 - `_) -- Fix CPython is not discovered from Windows registry, and discover pythons from Windows registry in decreasing order by - version - by :user:`gaborbernat`. (`#1796 `_) -- Fix symlink detection for creators - by :user:`asottile` (`#1803 `_) - -*********************** - v20.0.18 (2020-04-16) -*********************** - -Bugfixes - 20.0.18 -================== - -- Importing setuptools before cli_run could cause our python information query to fail due to setuptools patching - ``distutils.dist.Distribution`` - by :user:`gaborbernat`. (`#1771 `_) - -*********************** - v20.0.17 (2020-04-09) -*********************** - -Features - 20.0.17 -================== - -- Extend environment variables checked for configuration to also check aliases (e.g. setting either - ``VIRTUALENV_COPIES`` or ``VIRTUALENV_ALWAYS_COPY`` will work) - by :user:`gaborbernat`. (`#1763 - `_) - -*********************** - v20.0.16 (2020-04-04) -*********************** - -Bugfixes - 20.0.16 -================== - -- Allow seed wheel files inside the :option:`extra-search-dir` folders that do not have ``Requires-Python`` metadata - specified, these are considered compatible with all python versions - by :user:`gaborbernat`. (`#1757 - `_) - -*********************** - v20.0.15 (2020-03-27) -*********************** - -Features - 20.0.15 -================== - -- Upgrade embedded setuptools to ``46.1.3`` from ``46.1.1`` - by :user:`gaborbernat`. (`#1752 - `_) - -*********************** - v20.0.14 (2020-03-25) -*********************** - -Features - 20.0.14 -================== - -- Remove ``__PYVENV_LAUNCHER__`` on macOs for Python ``3.7.(<8)`` and ``3.8.(<3)`` on interpreter startup via ``pth`` - file, this pulls in the `upstream patch `_ - by :user:`gaborbernat`. - (`#1704 `_) -- Upgrade embedded setuptools for Python ``3.5+`` to ``46.1.1``, for Python ``2.7`` to ``44.1.0`` - by - :user:`gaborbernat`. (`#1745 `_) - -Bugfixes - 20.0.14 -================== - -- Fix discovery of interpreter by name from ``PATH`` that does not match a spec format - by :user:`gaborbernat`. (`#1746 - `_) - -*********************** - v20.0.13 (2020-03-19) -*********************** - -Bugfixes - 20.0.13 -================== - -- Do not fail when the pyc files is missing for the host Python 2 - by :user:`gaborbernat`. (`#1738 - `_) -- Support broken Packaging pythons that put the include headers under distutils pattern rather than sysconfig one - by - :user:`gaborbernat`. (`#1739 `_) - -*********************** - v20.0.12 (2020-03-19) -*********************** - -Bugfixes - 20.0.12 -================== - -- Fix relative path discovery of interpreters - by :user:`gaborbernat`. (`#1734 - `_) - -*********************** - v20.0.11 (2020-03-18) -*********************** - -Features - 20.0.11 -================== - -- Improve error message when the host python does not satisfy invariants needed to create virtual environments (now we - print which host files are incompatible/missing and for which creators when no supported creator can be matched, - however we found creators that can describe the given Python interpreter - will still print no supported creator for - Jython, however print exactly what host files do not allow creation of virtual environments in case of CPython/PyPy) - - by :user:`gaborbernat`. (`#1716 `_) - -Bugfixes - 20.0.11 -================== - -- Support Python 3 Framework distributed via XCode in macOs Catalina and before - by :user:`gaborbernat`. (`#1663 - `_) -- Fix Windows Store Python support, do not allow creation via symlink as that's not going to work by design - by - :user:`gaborbernat`. (`#1709 `_) -- Fix ``activate_this.py`` throws ``AttributeError`` on Windows when virtual environment was created via cross python - mechanism - by :user:`gaborbernat`. (`#1710 `_) -- Fix ``--no-pip``, ``--no-setuptools``, ``--no-wheel`` not being respected - by :user:`gaborbernat`. (`#1712 - `_) -- Allow missing ``.py`` files if a compiled ``.pyc`` version is available - by :user:`tucked`. (`#1714 - `_) -- Do not fail if the distutils/setuptools patch happens on a C-extension loader (such as ``zipimporter`` on Python 3.7 - or earlier) - by :user:`gaborbernat`. (`#1715 `_) -- Support Python 2 implementations that require the landmark files and ``site.py`` to be in platform standard library - instead of the standard library path of the virtual environment (notably some RHEL ones, such as the Docker image - ``amazonlinux:1``) - by :user:`gaborbernat`. (`#1719 `_) -- Allow the test suite to pass even when called with the system Python - to help repackaging of the tool for Linux - distributions - by :user:`gaborbernat`. (`#1721 `_) -- Also generate ``pipx.y`` console script beside ``pip-x.y`` to be compatible with how pip installs itself - by - :user:`gaborbernat`. (`#1723 `_) -- Automatically create the application data folder if it does not exists - by :user:`gaborbernat`. (`#1728 - `_) - -Improved Documentation - 20.0.11 -================================ - -- :ref:`supports ` details now explicitly what Python installations we support - by - :user:`gaborbernat`. (`#1714 `_) - -*********************** - v20.0.10 (2020-03-10) -*********************** - -Bugfixes - 20.0.10 -================== - -- Fix acquiring python information might be altered by distutils configuration files generating incorrect layout virtual - environments - by :user:`gaborbernat`. (`#1663 `_) -- Upgrade embedded setuptools to ``46.0.0`` from ``45.3.0`` on Python ``3.5+`` - by :user:`gaborbernat`. (`#1702 - `_) - -Improved Documentation - 20.0.10 -================================ - -- Document requirements (pip + index server) when installing via pip under the installation section - by - :user:`gaborbernat`. (`#1618 `_) -- Document installing from non PEP-518 systems - :user:`gaborbernat`. (`#1619 - `_) -- Document installing latest unreleased version from Github - :user:`gaborbernat`. (`#1620 - `_) - -********************** - v20.0.9 (2020-03-08) -********************** - -Bugfixes - 20.0.9 -================= - -- ``pythonw.exe`` works as ``python.exe`` on Windows - by :user:`gaborbernat`. (`#1686 - `_) -- Handle legacy loaders for virtualenv import hooks used to patch distutils configuration load - by :user:`gaborbernat`. - (`#1690 `_) -- Support for python 2 platforms that store landmark files in ``platstdlib`` over ``stdlib`` (e.g. RHEL) - by - :user:`gaborbernat`. (`#1694 `_) -- Upgrade embedded setuptools to ``45.3.0`` from ``45.2.0`` for Python ``3.5+`` - by :user:`gaborbernat`. (`#1699 - `_) - -********************** - v20.0.8 (2020-03-04) -********************** - -Bugfixes - 20.0.8 -================= - -- Having `distutils configuration `_ files - that set ``prefix`` and ``install_scripts`` cause installation of packages in the wrong location - by - :user:`gaborbernat`. (`#1663 `_) -- Fix ``PYTHONPATH`` being overridden on Python 2 โ€” by :user:`jd`. (`#1673 - `_) -- Fix list configuration value parsing from config file or environment variable - by :user:`gaborbernat`. (`#1674 - `_) -- Fix Batch activation script shell prompt to display environment name by default - by :user:`spetafree`. (`#1679 - `_) -- Fix startup on Python 2 is slower for virtualenv - this was due to setuptools calculating it's working set - distribution - by :user:`gaborbernat`. (`#1682 `_) -- Fix entry points are not populated for editable installs on Python 2 due to setuptools working set being calculated - before ``easy_install.pth`` runs - by :user:`gaborbernat`. (`#1684 `_) -- Fix ``attr:`` import fails for setuptools - by :user:`gaborbernat`. (`#1685 - `_) - -********************** - v20.0.7 (2020-02-26) -********************** - -Bugfixes - 20.0.7 -================= - -- Disable distutils fixup for python 3 until `pypa/pip #7778 `_ is fixed and - released - by :user:`gaborbernat`. (`#1669 `_) - -********************** - v20.0.6 (2020-02-26) -********************** - -Bugfixes - 20.0.6 -================= - -- Fix global site package always being added with bundled macOs python framework builds - by :user:`gaborbernat`. - (`#1561 `_) -- Fix generated scripts use host version info rather than target - by :user:`gaborbernat`. (`#1600 - `_) -- Fix circular prefix reference with single elements (accept these as if they were system executables, print a info - about them referencing themselves) - by :user:`gaborbernat`. (`#1632 - `_) -- Handle the case when the application data folder is read-only: - - - the application data folder is now controllable via :option:`app-data`, - - ``clear-app-data`` now cleans the entire application data folder, not just the ``app-data`` seeder path, - - check if the application data path passed in does not exist or is read-only, and fallback to a temporary directory, - - temporary directory application data is automatically cleaned up at the end of execution, - - :option:`symlink-app-data` is always ``False`` when the application data is temporary - - by :user:`gaborbernat`. (`#1640 `_) - -- Fix PyPy 2 builtin modules are imported from standard library, rather than from builtin - by :user:`gaborbernat`. - (`#1652 `_) -- Fix creation of entry points when path contains spaces - by :user:`nsoranzo`. (`#1660 - `_) -- Fix relative paths for the zipapp (for python ``3.7+``) - by :user:`gaborbernat`. (`#1666 - `_) - -********************** - v20.0.5 (2020-02-21) -********************** - -Features - 20.0.5 -================= - -- Also create ``pythonX.X`` executables when creating pypy virtualenvs - by :user:`asottile` (`#1612 - `_) -- Fail with better error message if trying to install source with unsupported ``setuptools``, allow ``setuptools-scm >= - 2`` and move to legacy ``setuptools-scm`` format to support better older platforms (``CentOS 7`` and such) - by - :user:`gaborbernat`. (`#1621 `_) -- Report of the created virtual environment is now split across four short lines rather than one long - by - :user:`gaborbernat` (`#1641 `_) - -Bugfixes - 20.0.5 -================= - -- Add macOs Python 2 Framework support (now we test it with the CI via brew) - by :user:`gaborbernat` (`#1561 - `_) -- Fix losing of libpypy-c.so when the pypy executable is a symlink - by :user:`asottile` (`#1614 - `_) -- Discover python interpreter in a case insensitive manner - by :user:`PrajwalM2212` (`#1624 - `_) -- Fix cross interpreter support when the host python sets ``sys.base_executable`` based on ``__PYVENV_LAUNCHER__`` - by - :user:`cjolowicz` (`#1643 `_) - -********************** - v20.0.4 (2020-02-14) -********************** - -Features - 20.0.4 -================= - -- When aliasing interpreters, use relative symlinks - by :user:`asottile`. (`#1596 - `_) - -Bugfixes - 20.0.4 -================= - -- Allow the use of ``/`` as pathname component separator on Windows - by ``vphilippon`` (`#1582 - `_) -- Lower minimal version of six required to 1.9 - by ``ssbarnea`` (`#1606 - `_) - -********************** - v20.0.3 (2020-02-12) -********************** - -Bugfixes - 20.0.3 -================= - -- On Python 2 with Apple Framework builds the global site package is no longer added when the - :option:`system-site-packages` is not specified - by :user:`gaborbernat`. (`#1561 - `_) -- Fix system python discovery mechanism when prefixes contain relative parts (e.g. ``..``) by resolving paths within the - python information query - by :user:`gaborbernat`. (`#1583 `_) -- Expose a programmatic API as ``from virtualenv import cli_run`` - by :user:`gaborbernat`. (`#1585 - `_) -- Fix ``app-data`` :option:`seeder` injects a extra ``.dist-info.virtualenv`` path that breaks ``importlib.metadata``, - now we inject an extra ``.virtualenv`` - by :user:`gaborbernat`. (`#1589 - `_) - -Improved Documentation - 20.0.3 -=============================== - -- Document a programmatic API as ``from virtualenv import cli_run`` under :ref:`programmatic_api` - by - :user:`gaborbernat`. (`#1585 `_) - -********************** - v20.0.2 (2020-02-11) -********************** - -Features - 20.0.2 -================= - -- Print out a one line message about the created virtual environment when no :option:`verbose` is set, this can now be - silenced to get back the original behavior via the :option:`quiet` flag - by :user:`pradyunsg`. (`#1557 - `_) -- Allow virtualenv's app data cache to be overridden by ``VIRTUALENV_OVERRIDE_APP_DATA`` - by :user:`asottile`. (`#1559 - `_) -- Passing in the virtual environment name/path is now required (no longer defaults to ``venv``) - by - :user:`gaborbernat`. (`#1568 `_) -- Add a CLI flag :option:`with-traceback` that allows displaying the stacktrace of the virtualenv when a failure occurs - - by :user:`gaborbernat`. (`#1572 `_) - -Bugfixes - 20.0.2 -================= - -- Support long path names for generated virtual environment console entry points (such as ``pip``) when using the - ``app-data`` :option:`seeder` - by :user:`gaborbernat`. (`#997 `_) -- Improve python discovery mechanism: - - - do not fail if there are executables that fail to query (e.g. for not having execute access to it) on the ``PATH``, - - beside the prefix folder also try with the platform dependent binary folder within that, - - by :user:`gaborbernat`. (`#1545 `_) - -- When copying (either files or trees) do not copy the permission bits, last access time, last modification time, and - flags as access to these might be forbidden (for example in case of the macOs Framework Python) and these are not - needed for the user to use the virtual environment - by :user:`gaborbernat`. (`#1561 - `_) -- While discovering a python executables interpreters that cannot be queried are now displayed with info level rather - than warning, so now they're no longer shown by default (these can be just executables to which we don't have access - or that are broken, don't warn if it's not the target Python we want) - by :user:`gaborbernat`. (`#1574 - `_) -- The ``app-data`` :option:`seeder` no longer symlinks the packages on UNIX and copies on Windows. Instead by default - always copies, however now has the :option:`symlink-app-data` flag allowing users to request this less robust but - faster method - by :user:`gaborbernat`. (`#1575 `_) - -Improved Documentation - 20.0.2 -=============================== - -- Add link to the `legacy documentation `_ for the changelog by :user:`jezdez`. - (`#1547 `_) -- Fine tune the documentation layout: default width of theme, allow tables to wrap around, soft corners for code - snippets - by :user:`pradyunsg`. (`#1548 `_) - -********************** - v20.0.1 (2020-02-10) -********************** - -Features - 20.0.1 -================= - -- upgrade embedded setuptools to ``45.2.0`` from ``45.1.0`` for Python ``3.4+`` - by :user:`gaborbernat`. (`#1554 - `_) - -Bugfixes - 20.0.1 -================= - -- Virtual environments created via relative path on Windows creates bad console executables - by :user:`gaborbernat`. - (`#1552 `_) -- Seems sometimes venvs created set their base executable to themselves; we accept these without question, so we handle - virtual environments as system pythons causing issues - by :user:`gaborbernat`. (`#1553 - `_) - -*********************** - v20.0.0. (2020-02-10) -*********************** - -Improved Documentation - 20.0.0. -================================ - -- Fixes typos, repeated words and inconsistent heading spacing. Rephrase parts of the development documentation and CLI - documentation. Expands shorthands like ``env var`` and ``config`` to their full forms. Uses descriptions from - respective documentation, for projects listed in ``related links`` - by :user:`pradyunsg`. (`#1540 - `_) - -************************ - v20.0.0b2 (2020-02-04) -************************ - -Features - 20.0.0b2 -=================== - -- Improve base executable discovery mechanism: - - - print at debug level why we refuse some candidates, - - when no candidates match exactly, instead of hard failing fallback to the closest match where the priority of - matching attributes is: python implementation, major version, minor version, architecture, patch version, release - level and serial (this is to facilitate things to still work when the OS upgrade replace/upgrades the system python - with a never version, than what the virtualenv host python was created with), - - always resolve system_executable information during the interpreter discovery, and the discovered environment is the - system interpreter instead of the venv/virtualenv (this happened before lazily the first time we accessed, and - caused reporting that the created virtual environment is of type of the virtualenv host python version, instead of - the system pythons version - these two can differ if the OS upgraded the system python underneath and the virtualenv - host was created via copy), - - by :user:`gaborbernat`. (`#1515 `_) - -- Generate ``bash`` and ``fish`` activators on Windows too (as these can be available with git bash, cygwin or mysys2) - - by :user:`gaborbernat`. (`#1527 `_) -- Upgrade the bundled ``wheel`` package from ``0.34.0`` to ``0.34.2`` - by :user:`gaborbernat`. (`#1531 - `_) - -Bugfixes - 20.0.0b2 -=================== - -- Bash activation script should have no extensions instead of ``.sh`` (this fixes the :pypi:`virtualenvwrapper` - integration) - by :user:`gaborbernat`. (`#1508 `_) -- Show less information when we run with a single verbosity (``-v``): - - - no longer shows accepted interpreters information (as the last proposed one is always the accepted one), - - do not display the ``str_spec`` attribute for ``PythonSpec`` as these can be deduced from the other attributes, - - for the ``app-data`` seeder do not show the type of lock, only the path to the app data directory, - - By :user:`gaborbernat`. (`#1510 `_) - -- Fixed cannot discover a python interpreter that has already been discovered under a different path (such is the case - when we have multiple symlinks to the same interpreter) - by :user:`gaborbernat`. (`#1512 - `_) -- Support relative paths for ``-p`` - by :user:`gaborbernat`. (`#1514 - `_) -- Creating virtual environments in parallel fail with cannot acquire lock within app data - by :user:`gaborbernat`. - (`#1516 `_) -- pth files were not processed under Debian CPython2 interpreters - by :user:`gaborbernat`. (`#1517 - `_) -- Fix prompt not displayed correctly with upcoming fish 3.10 due to us not preserving ``$pipestatus`` - by - :user:`krobelus`. (`#1530 `_) -- Stable order within ``pyenv.cfg`` and add ``include-system-site-packages`` only for creators that reference a global - Python - by user:`gaborbernat`. (`#1535 `_) - -Improved Documentation - 20.0.0b2 -================================= - -- Create the first iteration of the new documentation - by :user:`gaborbernat`. (`#1465 - `_) -- Project readme is now of type MarkDown instead of reStructuredText - by :user:`gaborbernat`. (`#1531 - `_) - -************************ - v20.0.0b1 (2020-01-28) -************************ - -- First public release of the rewrite. Everything is brand new and just added. -- ``--download`` defaults to ``False`` -- No longer replaces builtin ``site`` module with `custom version baked within virtualenv code itself - `_. A simple shim module is used to fix up - things on Python 2 only. - -.. warning:: - - The current virtualenv is the second iteration of implementation. From version ``0.8`` all the way to ``16.7.9`` we - numbered the first iteration. Version ``20.0.0b1`` is a complete rewrite of the package, and as such this release - history starts from there. The old changelog is still available in the `legacy branch documentation - `_. diff --git a/docs/changelog/3093.bugfix.rst b/docs/changelog/3093.bugfix.rst deleted file mode 100644 index c691a5fee..000000000 --- a/docs/changelog/3093.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Upgrade embedded wheels: - -- setuptools to ``82.0.1`` from ``82.0.0`` diff --git a/docs/changelog/examples.rst b/docs/changelog/examples.rst deleted file mode 100644 index 86257006a..000000000 --- a/docs/changelog/examples.rst +++ /dev/null @@ -1,23 +0,0 @@ -.. examples for changelog entries adding to your Pull Requests - -file ``544.doc.rst``: - -:: - - explain everything much better - by :user:`passionate_technicalwriter`. - -file ``544.feature.rst``: - -:: - - ``tox --version`` now shows information about all registered plugins - by :user:`obestwalter`. - -file ``571.bugfix.rst``: - -:: - - ``skip_install`` overrides ``usedevelop`` (``usedevelop`` is an option to choose the - installation type if the package is installed and ``skip_install`` determines if it should be - installed at all) - by :user:`ferdonline`. - -.. see pyproject.toml for all available categories diff --git a/docs/changelog/template.jinja2 b/docs/changelog/template.jinja2 deleted file mode 100644 index 9de49f34a..000000000 --- a/docs/changelog/template.jinja2 +++ /dev/null @@ -1,36 +0,0 @@ -{% if versiondata.name %} -{% set version_title = "v" + versiondata.version + " (" + versiondata.date + ")" %} -{% else %} -{% set version_title = versiondata.version + " (" + versiondata.date + ")" %} -{% endif %} -{{ top_underline * (version_title|length + 2) }} - {{ version_title }} -{{ top_underline * (version_title|length + 2) }} - -{% for section, _ in sections.items() %} -{% if sections[section] %} -{% for category, val in definitions.items() if category in sections[section]%} -{{ definitions[category]['name'] }} - {{ versiondata.version }} -{{ underlines[0] * ((definitions[category]['name'] + versiondata.version)|length + 3) }} - -{% if definitions[category]['showcontent'] %} -{% for text, values in sections[section][category].items() %} -- {{ text }} ({{ values|join(', ') }}) -{% endfor %} - -{% else %} -- {{ sections[section][category]['']|join(', ') }} - -{% endif %} -{% if sections[section][category]|length == 0 %} -No significant changes. - -{% else %} -{% endif %} -{% endfor %} -{% else %} -No significant changes. - - -{% endif %} -{% endfor %} diff --git a/docs/changes.rst b/docs/changes.rst new file mode 100644 index 000000000..1a591f94d --- /dev/null +++ b/docs/changes.rst @@ -0,0 +1,1023 @@ +Release History +=============== + +15.2.0 (unreleased) +------------------- + + +15.1.0 (2016-11-15) +------------------- + +* Support Python 3.6. + +* Upgrade setuptools to 28.0.0. + +* Upgrade pip to 9.0.1. + +* Don't install pre-release versions of pip, setuptools, or wheel from PyPI. + + +15.0.3 (2016-08-05) +------------------- + +* Test for given python path actually being an executable *file*, :issue:`939` + +* Only search for copy actual existing Tcl/Tk directories (:pull:`937`) + +* Generically search for correct Tcl/Tk version (:pull:`926`, :pull:`933`) + +* Upgrade setuptools to 22.0.5 + +15.0.2 (2016-05-28) +------------------- + +* Copy Tcl/Tk libs on Windows to allow them to run, + fixes :issue:`93` (:pull:`888`) + +* Upgrade setuptools to 21.2.1. + +* Upgrade pip to 8.1.2. + + +15.0.1 (2016-03-17) +------------------- + +* Print error message when DEST_DIR exists and is a file + +* Upgrade setuptools to 20.3 + +* Upgrade pip to 8.1.1. + + +15.0.0 (2016-03-05) +------------------- + +* Remove the `virtualenv-N.N` script from the package; this can no longer be + correctly created from a wheel installation. + Resolves :issue:`851`, :issue:`692` + +* Remove accidental runtime dependency on pip by extracting certificate in the + subprocess. + +* Upgrade setuptools 20.2.2. + +* Upgrade pip to 8.1.0. + + +14.0.6 (2016-02-07) +------------------- + +* Upgrade setuptools to 20.0 + +* Upgrade wheel to 0.29.0 + +* Fix an error where virtualenv didn't pass in a working ssl certificate for + pip, causing "weird" errors related to ssl. + + +14.0.5 (2016-02-01) +------------------- + +* Homogenize drive letter casing for both prefixes and filenames. :issue:`858` + + +14.0.4 (2016-01-31) +------------------- + +* Upgrade setuptools to 19.6.2 + +* Revert ac4ea65; only correct drive letter case. + Fixes :issue:`856`, :issue:`815` + + +14.0.3 (2016-01-28) +------------------- + +* Upgrade setuptools to 19.6.1 + + +14.0.2 (2016-01-28) +------------------- + +* Upgrade setuptools to 19.6 + +* Supress any errors from `unset` on different shells (:pull:`843`) + +* Normalize letter case for prefix path checking. Fixes :issue:`837` + + +14.0.1 (2016-01-21) +------------------- + +* Upgrade from pip 8.0.0 to 8.0.2. + +* Fix the default of ``--(no-)download`` to default to downloading. + + +14.0.0 (2016-01-19) +------------------- + +* **BACKWARDS INCOMPATIBLE** Drop support for Python 3.2. + +* Upgrade setuptools to 19.4 + +* Upgrade wheel to 0.26.0 + +* Upgrade pip to 8.0.0 + +* Upgrade argparse to 1.4.0 + +* Added support for ``python-config`` script (:pull:`798`) + +* Updated activate.fish (:pull:`589`) (:pull:`799`) + +* Account for a ``site.pyo`` correctly in some python implementations (:pull:`759`) + +* Properly restore an empty PS1 (:issue:`407`) + +* Properly remove ``pydoc`` when deactivating + +* Remove workaround for very old Mageia / Mandriva linuxes (:pull:`472`) + +* Added a space after virtualenv name in the prompt: ``(env) $PS1`` + +* Make sure not to run a --user install when creating the virtualenv (:pull:`803`) + +* Remove virtualenv.py's path from sys.path when executing with a new + python. Fixes issue :issue:`779`, :issue:`763` (:pull:`805`) + +* Remove use of () in .bat files so ``Program Files (x86)`` works :issue:`35` + +* Download new releases of the preinstalled software from PyPI when there are + new releases available. This behavior can be disabled using + ``--no-download``. + +* Make ``--no-setuptools``, ``--no-pip``, and ``--no-wheel`` independent of + each other. + + +13.1.2 (2015-08-23) +------------------- + +* Upgrade pip to 7.1.2. + + +13.1.1 (2015-08-20) +------------------- + +* Upgrade pip to 7.1.1. + +* Upgrade setuptools to 18.2. + +* Make the activate script safe to use when bash is running with ``-u``. + + +13.1.0 (2015-06-30) +------------------- + +* Upgrade pip to 7.1.0 + +* Upgrade setuptools to 18.0.1 + + +13.0.3 (2015-06-01) +------------------- + +* Upgrade pip to 7.0.3 + + +13.0.2 (2015-06-01) +------------------- + +* Upgrade pip to 7.0.2 + +* Upgrade setuptools to 17.0 + + +13.0.1 (2015-05-22) +------------------- + +* Upgrade pip to 7.0.1 + + +13.0.0 (2015-05-21) +------------------- + +* Automatically install wheel when creating a new virutalenv. This can be + disabled by using the ``--no-wheel`` option. + +* Don't trust the current directory as a location to discover files to install + packages from. + +* Upgrade setuptools to 16.0. + +* Upgrade pip to 7.0.0. + + +12.1.1 (2015-04-07) +------------------- + +* Upgrade pip to 6.1.1 + + +12.1.0 (2015-04-07) +------------------- + +* Upgrade setuptools to 15.0 + +* Upgrade pip to 6.1.0 + + +12.0.7 (2015-02-04) +------------------- + +* Upgrade pip to 6.0.8 + + +12.0.6 (2015-01-28) +------------------- + +* Upgrade pip to 6.0.7 + +* Upgrade setuptools to 12.0.5 + + +12.0.5 (2015-01-03) +------------------- + +* Upgrade pip to 6.0.6 + +* Upgrade setuptools to 11.0 + + +12.0.4 (2014-12-23) +------------------- + +* Revert the fix to ``-p`` on Debian based pythons as it was broken in other + situations. + +* Revert several sys.path changes new in 12.0 which were breaking virtualenv. + +12.0.3 (2014-12-23) +------------------- + +* Fix an issue where Debian based Pythons would fail when using -p with the + host Python. + +* Upgrade pip to 6.0.3 + +12.0.2 (2014-12-23) +------------------- + +* Upgraded pip to 6.0.2 + +12.0.1 (2014-12-22) +------------------- + +* Upgraded pip to 6.0.1 + + +12.0 (2014-12-22) +----------------- + +* **PROCESS** Version numbers are now simply ``X.Y`` where the leading ``1`` + has been dropped. +* Split up documentation into structured pages +* Now using pytest framework +* Correct sys.path ordering for debian, issue #461 +* Correctly throws error on older Pythons, issue #619 +* Allow for empty $PATH, pull #601 +* Don't set prompt if $env:VIRTUAL_ENV_DISABLE_PROMPT is set for Powershell +* Updated setuptools to 7.0 + +1.11.6 (2014-05-16) +------------------- + +* Updated setuptools to 3.6 +* Updated pip to 1.5.6 + +1.11.5 (2014-05-03) +------------------- + +* Updated setuptools to 3.4.4 +* Updated documentation to use https://virtualenv.pypa.io/ +* Updated pip to 1.5.5 + +1.11.4 (2014-02-21) +------------------- + +* Updated pip to 1.5.4 + + +1.11.3 (2014-02-20) +------------------- + +* Updated setuptools to 2.2 +* Updated pip to 1.5.3 + + +1.11.2 (2014-01-26) +------------------- + +* Fixed easy_install installed virtualenvs by updated pip to 1.5.2 + +1.11.1 (2014-01-20) +------------------- + +* Fixed an issue where pip and setuptools were not getting installed when using + the ``--system-site-packages`` flag. +* Updated setuptools to fix an issue when installed with easy_install +* Fixed an issue with Python 3.4 and sys.stdout encoding being set to ascii +* Upgraded pip to v1.5.1 +* Upgraded setuptools to v2.1 + +1.11 (2014-01-02) +----------------- + +* **BACKWARDS INCOMPATIBLE** Switched to using wheels for the bundled copies of + setuptools and pip. Using sdists is no longer supported - users supplying + their own versions of pip/setuptools will need to provide wheels. +* **BACKWARDS INCOMPATIBLE** Modified the handling of ``--extra-search-dirs``. + This option now works like pip's ``--find-links`` option, in that it adds + extra directories to search for compatible wheels for pip and setuptools. + The actual wheel selected is chosen based on version and compatibility, using + the same algorithm as ``pip install setuptools``. +* Fixed #495, --always-copy was failing (#PR 511) +* Upgraded pip to v1.5 +* Upgraded setuptools to v1.4 + +1.10.1 (2013-08-07) +------------------- + +* **New Signing Key** Release 1.10.1 is using a different key than normal with + fingerprint: 7C6B 7C5D 5E2B 6356 A926 F04F 6E3C BCE9 3372 DCFA +* Upgraded pip to v1.4.1 +* Upgraded setuptools to v0.9.8 + + +1.10 (2013-07-23) +----------------- + +* **BACKWARDS INCOMPATIBLE** Dropped support for Python 2.5. The minimum + supported Python version is now Python 2.6. + +* **BACKWARDS INCOMPATIBLE** Using ``virtualenv.py`` as an isolated script + (i.e. without an associated ``virtualenv_support`` directory) is no longer + supported for security reasons and will fail with an error. + + Along with this, ``--never-download`` is now always pinned to ``True``, and + is only being maintained in the short term for backward compatibility + (Pull #412). + +* **IMPORTANT** Switched to the new setuptools (v0.9.7) which has been merged + with Distribute_ again and works for Python 2 and 3 with one codebase. + The ``--distribute`` and ``--setuptools`` options are now no-op. + +* Updated to pip 1.4. + +* Added support for PyPy3k + +* Added the option to use a version number with the ``-p`` option to get the + system copy of that Python version (Windows only) + +* Removed embedded ``ez_setup.py``, ``distribute_setup.py`` and + ``distribute_from_egg.py`` files as part of switching to merged setuptools. + +* Fixed ``--relocatable`` to work better on Windows. + +* Fixed issue with readline on Windows. + +.. _Distribute: https://pypi.python.org/pypi/distribute + +1.9.1 (2013-03-08) +------------------ + +* Updated to pip 1.3.1 that fixed a major backward incompatible change of + parsing URLs to externally hosted packages that got accidentily included + in pip 1.3. + +1.9 (2013-03-07) +---------------- + +* Unset VIRTUAL_ENV environment variable in deactivate.bat (Pull #364) +* Upgraded distribute to 0.6.34. +* Added ``--no-setuptools`` and ``--no-pip`` options (Pull #336). +* Fixed Issue #373. virtualenv-1.8.4 was failing in cygwin (Pull #382). +* Fixed Issue #378. virtualenv is now "multiarch" aware on debian/ubuntu (Pull #379). +* Fixed issue with readline module path on pypy and OSX (Pull #374). +* Made 64bit detection compatible with Python 2.5 (Pull #393). + + +1.8.4 (2012-11-25) +------------------ + +* Updated distribute to 0.6.31. This fixes #359 (numpy install regression) on + UTF-8 platforms, and provides a workaround on other platforms: + ``PYTHONIOENCODING=utf8 pip install numpy``. + +* When installing virtualenv via curl, don't forget to filter out arguments + the distribute setup script won't understand. Fixes #358. + +* Added some more integration tests. + +* Removed the unsupported embedded setuptools egg for Python 2.4 to reduce + file size. + +1.8.3 (2012-11-21) +------------------ + +* Fixed readline on OS X. Thanks minrk + +* Updated distribute to 0.6.30 (improves our error reporting, plus new + distribute features and fixes). Thanks Gabriel (g2p) + +* Added compatibility with multiarch Python (Python 3.3 for example). Added an + integration test. Thanks Gabriel (g2p) + +* Added ability to install distribute from a user-provided egg, rather than the + bundled sdist, for better speed. Thanks Paul Moore. + +* Make the creation of lib64 symlink smarter about already-existing symlink, + and more explicit about full paths. Fixes #334 and #330. Thanks Jeremy Orem. + +* Give lib64 site-dir preference over lib on 64-bit systems, to avoid wrong + 32-bit compiles in the venv. Fixes #328. Thanks Damien Nozay. + +* Fix a bug with prompt-handling in ``activate.csh`` in non-interactive csh + shells. Fixes #332. Thanks Benjamin Root for report and patch. + +* Make it possible to create a virtualenv from within a Python + 3.3. pyvenv. Thanks Chris McDonough for the report. + +* Add optional --setuptools option to be able to switch to it in case + distribute is the default (like in Debian). + +1.8.2 (2012-09-06) +------------------ + +* Updated the included pip version to 1.2.1 to fix regressions introduced + there in 1.2. + + +1.8.1 (2012-09-03) +------------------ + +* Fixed distribute version used with `--never-download`. Thanks michr for + report and patch. + +* Fix creating Python 3.3 based virtualenvs by unsetting the + ``__PYVENV_LAUNCHER__`` environment variable in subprocesses. + + +1.8 (2012-09-01) +---------------- + +* **Dropped support for Python 2.4** The minimum supported Python version is + now Python 2.5. + +* Fix `--relocatable` on systems that use lib64. Fixes #78. Thanks Branden + Rolston. + +* Symlink some additional modules under Python 3. Fixes #194. Thanks Vinay + Sajip, Ian Clelland, and Stefan Holek for the report. + +* Fix ``--relocatable`` when a script uses ``__future__`` imports. Thanks + Branden Rolston. + +* Fix a bug in the config option parser that prevented setting negative + options with environment variables. Thanks Ralf Schmitt. + +* Allow setting ``--no-site-packages`` from the config file. + +* Use ``/usr/bin/multiarch-platform`` if available to figure out the include + directory. Thanks for the patch, Mika Laitio. + +* Fix ``install_name_tool`` replacement to work on Python 3.X. + +* Handle paths of users' site-packages on Mac OS X correctly when changing + the prefix. + +* Updated the embedded version of distribute to 0.6.28 and pip to 1.2. + + +1.7.2 (2012-06-22) +------------------ + +* Updated to distribute 0.6.27. + +* Fix activate.fish on OS X. Fixes #8. Thanks David Schoonover. + +* Create a virtualenv-x.x script with the Python version when installing, so + virtualenv for multiple Python versions can be installed to the same + script location. Thanks Miki Tebeka. + +* Restored ability to create a virtualenv with a path longer than 78 + characters, without breaking creation of virtualenvs with non-ASCII paths. + Thanks, Bradley Ayers. + +* Added ability to create virtualenvs without having installed Apple's + developers tools (using an own implementation of ``install_name_tool``). + Thanks Mike Hommey. + +* Fixed PyPy and Jython support on Windows. Thanks Konstantin Zemlyak. + +* Added pydoc script to ease use. Thanks Marc Abramowitz. Fixes #149. + +* Fixed creating a bootstrap script on Python 3. Thanks Raul Leal. Fixes #280. + +* Fixed inconsistency when having set the ``PYTHONDONTWRITEBYTECODE`` env var + with the --distribute option or the ``VIRTUALENV_USE_DISTRIBUTE`` env var. + ``VIRTUALENV_USE_DISTRIBUTE`` is now considered again as a legacy alias. + + +1.7.1.2 (2012-02-17) +-------------------- + +* Fixed minor issue in `--relocatable`. Thanks, Cap Petschulat. + + +1.7.1.1 (2012-02-16) +-------------------- + +* Bumped the version string in ``virtualenv.py`` up, too. + +* Fixed rST rendering bug of long description. + + +1.7.1 (2012-02-16) +------------------ + +* Update embedded pip to version 1.1. + +* Fix `--relocatable` under Python 3. Thanks Doug Hellmann. + +* Added environ PATH modification to activate_this.py. Thanks Doug + Napoleone. Fixes #14. + +* Support creating virtualenvs directly from a Python build directory on + Windows. Thanks CBWhiz. Fixes #139. + +* Use non-recursive symlinks to fix things up for posix_local install + scheme. Thanks michr. + +* Made activate script available for use with msys and cygwin on Windows. + Thanks Greg Haskins, Cliff Xuan, Jonathan Griffin and Doug Napoleone. + Fixes #176. + +* Fixed creation of virtualenvs on Windows when Python is not installed for + all users. Thanks Anatoly Techtonik for report and patch and Doug + Napoleone for testing and confirmation. Fixes #87. + +* Fixed creation of virtualenvs using -p in installs where some modules + that ought to be in the standard library (e.g. `readline`) are actually + installed in `site-packages` next to `virtualenv.py`. Thanks Greg Haskins + for report and fix. Fixes #167. + +* Added activation script for Powershell (signed by Jannis Leidel). Many + thanks to Jason R. Coombs. + + +1.7 (2011-11-30) +---------------- + +* Gave user-provided ``--extra-search-dir`` priority over default dirs for + finding setuptools/distribute (it already had priority for finding pip). + Thanks Ethan Jucovy. + +* Updated embedded Distribute release to 0.6.24. Thanks Alex Gronholm. + +* Made ``--no-site-packages`` behavior the default behavior. The + ``--no-site-packages`` flag is still permitted, but displays a warning when + used. Thanks Chris McDonough. + +* New flag: ``--system-site-packages``; this flag should be passed to get the + previous default global-site-package-including behavior back. + +* Added ability to set command options as environment variables and options + in a ``virtualenv.ini`` file. + +* Fixed various encoding related issues with paths. Thanks Gunnlaugur Thor Briem. + +* Made ``virtualenv.py`` script executable. + + +1.6.4 (2011-07-21) +------------------ + +* Restored ability to run on Python 2.4, too. + + +1.6.3 (2011-07-16) +------------------ + +* Restored ability to run on Python < 2.7. + + +1.6.2 (2011-07-16) +------------------ + +* Updated embedded distribute release to 0.6.19. + +* Updated embedded pip release to 1.0.2. + +* Fixed #141 - Be smarter about finding pkg_resources when using the + non-default Python interpreter (by using the ``-p`` option). + +* Fixed #112 - Fixed path in docs. + +* Fixed #109 - Corrected doctests of a Logger method. + +* Fixed #118 - Fixed creating virtualenvs on platforms that use the + "posix_local" install scheme, such as Ubuntu with Python 2.7. + +* Add missing library to Python 3 virtualenvs (``_dummy_thread``). + + +1.6.1 (2011-04-30) +------------------ + +* Start to use git-flow. + +* Added support for PyPy 1.5 + +* Fixed #121 -- added sanity-checking of the -p argument. Thanks Paul Nasrat. + +* Added progress meter for pip installation as well as setuptools. Thanks Ethan + Jucovy. + +* Added --never-download and --search-dir options. Thanks Ethan Jucovy. + + +1.6 +--- + +* Added Python 3 support! Huge thanks to Vinay Sajip and Vitaly Babiy. + +* Fixed creation of virtualenvs on Mac OS X when standard library modules + (readline) are installed outside the standard library. + +* Updated bundled pip to 1.0. + + +1.5.2 +----- + +* Moved main repository to Github: https://github.com/pypa/virtualenv + +* Transferred primary maintenance from Ian to Jannis Leidel, Carl Meyer and Brian Rosner + +* Fixed a few more pypy related bugs. + +* Updated bundled pip to 0.8.2. + +* Handed project over to new team of maintainers. + +* Moved virtualenv to Github at https://github.com/pypa/virtualenv + + +1.5.1 +----- + +* Added ``_weakrefset`` requirement for Python 2.7.1. + +* Fixed Windows regression in 1.5 + + +1.5 +--- + +* Include pip 0.8.1. + +* Add support for PyPy. + +* Uses a proper temporary dir when installing environment requirements. + +* Add ``--prompt`` option to be able to override the default prompt prefix. + +* Fix an issue with ``--relocatable`` on Windows. + +* Fix issue with installing the wrong version of distribute. + +* Add fish and csh activate scripts. + + +1.4.9 +----- + +* Include pip 0.7.2 + + +1.4.8 +----- + +* Fix for Mac OS X Framework builds that use + ``--universal-archs=intel`` + +* Fix ``activate_this.py`` on Windows. + +* Allow ``$PYTHONHOME`` to be set, so long as you use ``source + bin/activate`` it will get unset; if you leave it set and do not + activate the environment it will still break the environment. + +* Include pip 0.7.1 + + +1.4.7 +----- + +* Include pip 0.7 + + +1.4.6 +----- + +* Allow ``activate.sh`` to skip updating the prompt (by setting + ``$VIRTUAL_ENV_DISABLE_PROMPT``). + + +1.4.5 +----- + +* Include pip 0.6.3 + +* Fix ``activate.bat`` and ``deactivate.bat`` under Windows when + ``PATH`` contained a parenthesis + + +1.4.4 +----- + +* Include pip 0.6.2 and Distribute 0.6.10 + +* Create the ``virtualenv`` script even when Setuptools isn't + installed + +* Fix problem with ``virtualenv --relocate`` when ``bin/`` has + subdirectories (e.g., ``bin/.svn/``); from Alan Franzoni. + +* If you set ``$VIRTUALENV_DISTRIBUTE`` then virtualenv will use + Distribute by default (so you don't have to remember to use + ``--distribute``). + + +1.4.3 +----- + +* Include pip 0.6.1 + + +1.4.2 +----- + +* Fix pip installation on Windows + +* Fix use of stand-alone ``virtualenv.py`` (and boot scripts) + +* Exclude ~/.local (user site-packages) from environments when using + ``--no-site-packages`` + + +1.4.1 +----- + +* Include pip 0.6 + + +1.4 +--- + +* Updated setuptools to 0.6c11 + +* Added the --distribute option + +* Fixed packaging problem of support-files + + +1.3.4 +----- + +* Virtualenv now copies the actual embedded Python binary on + Mac OS X to fix a hang on Snow Leopard (10.6). + +* Fail more gracefully on Windows when ``win32api`` is not installed. + +* Fix site-packages taking precedent over Jython's ``__classpath__`` + and also specially handle the new ``__pyclasspath__`` entry in + ``sys.path``. + +* Now copies Jython's ``registry`` file to the virtualenv if it exists. + +* Better find libraries when compiling extensions on Windows. + +* Create ``Scripts\pythonw.exe`` on Windows. + +* Added support for the Debian/Ubuntu + ``/usr/lib/pythonX.Y/dist-packages`` directory. + +* Set ``distutils.sysconfig.get_config_vars()['LIBDIR']`` (based on + ``sys.real_prefix``) which is reported to help building on Windows. + +* Make ``deactivate`` work on ksh + +* Fixes for ``--python``: make it work with ``--relocatable`` and the + symlink created to the exact Python version. + + +1.3.3 +----- + +* Use Windows newlines in ``activate.bat``, which has been reported to help + when using non-ASCII directory names. + +* Fixed compatibility with Jython 2.5b1. + +* Added a function ``virtualenv.install_python`` for more fine-grained + access to what ``virtualenv.create_environment`` does. + +* Fix `a problem `_ + with Windows and paths that contain spaces. + +* If ``/path/to/env/.pydistutils.cfg`` exists (or + ``/path/to/env/pydistutils.cfg`` on Windows systems) then ignore + ``~/.pydistutils.cfg`` and use that other file instead. + +* Fix ` a problem + `_ picking up + some ``.so`` libraries in ``/usr/local``. + + +1.3.2 +----- + +* Remove the ``[install] prefix = ...`` setting from the virtualenv + ``distutils.cfg`` -- this has been causing problems for a lot of + people, in rather obscure ways. + +* If you use a boot script it will attempt to import ``virtualenv`` + and find a pre-downloaded Setuptools egg using that. + +* Added platform-specific paths, like ``/usr/lib/pythonX.Y/plat-linux2`` + + +1.3.1 +----- + +* Real Python 2.6 compatibility. Backported the Python 2.6 updates to + ``site.py``, including `user directories + `_ + (this means older versions of Python will support user directories, + whether intended or not). + +* Always set ``[install] prefix`` in ``distutils.cfg`` -- previously + on some platforms where a system-wide ``distutils.cfg`` was present + with a ``prefix`` setting, packages would be installed globally + (usually in ``/usr/local/lib/pythonX.Y/site-packages``). + +* Sometimes Cygwin seems to leave ``.exe`` off ``sys.executable``; a + workaround is added. + +* Fix ``--python`` option. + +* Fixed handling of Jython environments that use a + jython-complete.jar. + + +1.3 +--- + +* Update to Setuptools 0.6c9 +* Added an option ``virtualenv --relocatable EXISTING_ENV``, which + will make an existing environment "relocatable" -- the paths will + not be absolute in scripts, ``.egg-info`` and ``.pth`` files. This + may assist in building environments that can be moved and copied. + You have to run this *after* any new packages installed. +* Added ``bin/activate_this.py``, a file you can use like + ``execfile("path_to/activate_this.py", + dict(__file__="path_to/activate_this.py"))`` -- this will activate + the environment in place, similar to what `the mod_wsgi example + does `_. +* For Mac framework builds of Python, the site-packages directory + ``/Library/Python/X.Y/site-packages`` is added to ``sys.path``, from + Andrea Rech. +* Some platform-specific modules in Macs are added to the path now + (``plat-darwin/``, ``plat-mac/``, ``plat-mac/lib-scriptpackages``), + from Andrea Rech. +* Fixed a small Bashism in the ``bin/activate`` shell script. +* Added ``__future__`` to the list of required modules, for Python + 2.3. You'll still need to backport your own ``subprocess`` module. +* Fixed the ``__classpath__`` entry in Jython's ``sys.path`` taking + precedent over virtualenv's libs. + + +1.2 +--- + +* Added a ``--python`` option to select the Python interpreter. +* Add ``warnings`` to the modules copied over, for Python 2.6 support. +* Add ``sets`` to the module copied over for Python 2.3 (though Python + 2.3 still probably doesn't work). + + +1.1.1 +----- + +* Added support for Jython 2.5. + + +1.1 +--- + +* Added support for Python 2.6. +* Fix a problem with missing ``DLLs/zlib.pyd`` on Windows. Create +* ``bin/python`` (or ``bin/python.exe``) even when you run virtualenv + with an interpreter named, e.g., ``python2.4`` +* Fix MacPorts Python +* Added --unzip-setuptools option +* Update to Setuptools 0.6c8 +* If the current directory is not writable, run ez_setup.py in ``/tmp`` +* Copy or symlink over the ``include`` directory so that packages will + more consistently compile. + + +1.0 +--- + +* Fix build on systems that use ``/usr/lib64``, distinct from + ``/usr/lib`` (specifically CentOS x64). +* Fixed bug in ``--clear``. +* Fixed typos in ``deactivate.bat``. +* Preserve ``$PYTHONPATH`` when calling subprocesses. + + +0.9.2 +----- + +* Fix include dir copying on Windows (makes compiling possible). +* Include the main ``lib-tk`` in the path. +* Patch ``distutils.sysconfig``: ``get_python_inc`` and + ``get_python_lib`` to point to the global locations. +* Install ``distutils.cfg`` before Setuptools, so that system + customizations of ``distutils.cfg`` won't effect the installation. +* Add ``bin/pythonX.Y`` to the virtualenv (in addition to + ``bin/python``). +* Fixed an issue with Mac Framework Python builds, and absolute paths + (from Ronald Oussoren). + + +0.9.1 +----- + +* Improve ability to create a virtualenv from inside a virtualenv. +* Fix a little bug in ``bin/activate``. +* Actually get ``distutils.cfg`` to work reliably. + + +0.9 +--- + +* Added ``lib-dynload`` and ``config`` to things that need to be + copied over in an environment. +* Copy over or symlink the ``include`` directory, so that you can + build packages that need the C headers. +* Include a ``distutils`` package, so you can locally update + ``distutils.cfg`` (in ``lib/pythonX.Y/distutils/distutils.cfg``). +* Better avoid downloading Setuptools, and hitting PyPI on environment + creation. +* Fix a problem creating a ``lib64/`` directory. +* Should work on MacOSX Framework builds (the default Python + installations on Mac). Thanks to Ronald Oussoren. + + +0.8.4 +----- + +* Windows installs would sometimes give errors about ``sys.prefix`` that + were inaccurate. +* Slightly prettier output. + + +0.8.3 +----- + +* Added support for Windows. + + +0.8.2 +----- + +* Give a better warning if you are on an unsupported platform (Mac + Framework Pythons, and Windows). +* Give error about running while inside a workingenv. +* Give better error message about Python 2.3. + + +0.8.1 +----- + +Fixed packaging of the library. + + +0.8 +--- + +Initial release. Everything is changed and new! diff --git a/docs/conf.py b/docs/conf.py index 065d48e00..9332aa1bc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,87 +1,153 @@ -from __future__ import annotations - +# -*- coding: utf-8 -*- +# +# Paste documentation build configuration file, created by +# sphinx-quickstart on Tue Apr 22 22:08:49 2008. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# The contents of this file are pickled, so don't put values in the namespace +# that aren't pickleable (module imports are okay, they're removed automatically). +# +# All configuration values have a default value; values that are commented out +# serve to show the default value. + +import os import sys -from datetime import datetime, timezone -from pathlib import Path - -from virtualenv.version import __version__ - -company = "PyPA" -name = "virtualenv" -version = ".".join(__version__.split(".")[:2]) -release = __version__ -copyright = f"2007-{datetime.now(tz=timezone.utc).year}, {company}, PyPA" # noqa: A001 - -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.autosectionlabel", - "sphinx.ext.extlinks", - "sphinx.ext.intersphinx", - "sphinx_autodoc_typehints", - "sphinx_copybutton", - "sphinx_inline_tabs", - "sphinxcontrib.mermaid", - "sphinxcontrib.towncrier.ext", -] - -intersphinx_mapping = { - "python": ("https://docs.python.org/3", None), -} -towncrier_draft_autoversion_mode = "draft" -towncrier_draft_include_empty = True -towncrier_draft_working_directory = Path(__file__).parent.parent +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' + +# If your extensions are in another directory, add it here. +sys.path.insert(0, os.path.abspath(os.pardir)) + +# General configuration +# --------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.extlinks'] + +# Add any paths that contain templates here, relative to this directory. +#templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' -templates_path = [] +# General substitutions. +project = 'virtualenv' +copyright = '2007-2014, Ian Bicking, The Open Planning Project, PyPA' + +# The default replacements for |version| and |release|, also used in various +# other places throughout the built documents. +try: + from virtualenv import __version__ + # The short X.Y version. + version = '.'.join(__version__.split('.')[:2]) + # The full version, including alpha/beta/rc tags. + release = __version__ +except ImportError: + version = release = 'dev' + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. unused_docs = [] -source_suffix = ".rst" -exclude_patterns = ["_build", "changelog/*", "_draft.rst"] - -main_doc = "index" -pygments_style = "default" -always_document_param_types = True -project = name -today_fmt = "%B %d, %Y" - -html_theme = "furo" -html_title, html_last_updated_fmt = project, datetime.now(tz=timezone.utc).isoformat() -pygments_style, pygments_dark_style = "sphinx", "monokai" -html_static_path = ["_static"] -html_css_files = ["custom.css"] -html_js_files = ["rtd-search.js"] -html_favicon = "_static/virtualenv.svg" -html_theme_options = { - "light_logo": "virtualenv.png", - "dark_logo": "virtualenv.png", - "sidebar_hide_name": True, -} -html_show_sourcelink = False -autoclass_content = "both" # Include __init__ in class documentation -autodoc_member_order = "bysource" -autosectionlabel_prefix_document = True +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' extlinks = { - "issue": ("https://github.com/pypa/virtualenv/issues/%s", "#%s"), - "pull": ("https://github.com/pypa/virtualenv/pull/%s", "PR #%s"), - "user": ("https://github.com/%s", "@%s"), - "pypi": ("https://pypi.org/project/%s", "%s"), + 'issue': ('https://github.com/pypa/virtualenv/issues/%s', '#'), + 'pull': ('https://github.com/pypa/virtualenv/pull/%s', 'PR #'), } -def setup(app) -> None: - doc_tree = Path(app.doctreedir) - for name in ("cli_interface", "reference/cli"): - doctree = doc_tree / f"{name}.doctree" - if doctree.exists(): - doctree.unlink() +# Options for HTML output +# ----------------------- + +# The style sheet to use for HTML and HTML Help pages. A file of that name +# must exist either in Sphinx' static/ path, or in one of the custom paths +# given in html_static_path. +#html_style = 'default.css' + +html_theme = 'default' +if not on_rtd: + try: + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + except ImportError: + pass + + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Content template for the index page. +#html_index = '' + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_use_modindex = True + +# If true, the reST sources are included in the HTML build as _sources/. +#html_copy_source = True + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Pastedoc' + + +# Options for LaTeX output +# ------------------------ + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, document class [howto/manual]). +#latex_documents = [] - here = Path(__file__).parent - if str(here) not in sys.path: - sys.path.append(str(here)) +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' - from render_cli import CliTable, literal_data # noqa: PLC0415 +# Documents to append as an appendix to all manuals. +#latex_appendices = [] - app.add_css_file("custom.css") - app.add_directive(CliTable.name, CliTable) - app.add_role("literal_data", literal_data) +# If false, no module index is generated. +#latex_use_modindex = True diff --git a/docs/development.rst b/docs/development.rst index af18e9b89..aba2785a3 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -1,274 +1,61 @@ -############# - Development -############# +Development +=========== -***************** - Getting started -***************** +Contributing +------------ -``virtualenv`` is a volunteer maintained open source project and we welcome contributions of all forms. The sections -below will help you get started with development, testing, and documentation. Weโ€™re pleased that you are interested in -working on virtualenv. This document is meant to get you setup to work on virtualenv and to act as a guide and reference -to the development setup. If you face any issues during this process, please `open an issue -`_ about it on the issue -tracker. +Refer to the `pip development`_ documentation - it applies equally to +virtualenv, except that virtualenv issues should filed on the `virtualenv +repo`_ at GitHub. -Setup -===== +Virtualenv's release schedule is tied to pip's -- each time there's a new pip +release, there will be a new virtualenv release that bundles the new version of +pip. -virtualenv is a command line application written in Python. To work on it, you'll need: +Files in the `virtualenv_embedded/` subdirectory are embedded into +`virtualenv.py` itself as base64-encoded strings (in order to support +single-file use of `virtualenv.py` without installing it). If your patch +changes any file in `virtualenv_embedded/`, run `bin/rebuild-script.py` to +update the embedded version of that file in `virtualenv.py`; commit that and +submit it as part of your patch / pull request. -- **Source code**: available on `GitHub `_. You can use ``git`` to clone the - repository: +.. _pip development: http://www.pip-installer.org/en/latest/development.html +.. _virtualenv repo: https://github.com/pypa/virtualenv/ - .. code-block:: console +Running the tests +----------------- - git clone https://github.com/pypa/virtualenv - cd virtualenv +Virtualenv's test suite is small and not yet at all comprehensive, but we aim +to grow it. -- **Python interpreter**: We recommend using ``CPython``. You can use `this guide - `_ to set it up. -- :pypi:`tox`: to automatically get the projects development dependencies and run the test suite. We recommend - installing it using `pipx `_. +The easy way to run tests (handles test dependencies automatically):: -Running from source tree -======================== + $ python setup.py test -The easiest way to do this is to generate the development tox environment, and then invoke virtualenv from under the -``.tox/dev`` folder +If you want to run only a selection of the tests, you'll need to run them +directly with pytest instead. Create a virtualenv, and install required +packages:: -.. code-block:: console + $ pip install pytest mock - tox -e dev - .tox/dev/bin/virtualenv # on Linux - .tox/dev/Scripts/virtualenv # on Windows +Run pytest:: -Running tests -============= + $ pytest -virtualenv's tests are written using the :pypi:`pytest` test framework. :pypi:`tox` is used to automate the setup and -execution of virtualenv's tests. +Or select just a single test file to run:: -To run tests locally execute: + $ pytest tests/test_virtualenv -.. code-block:: console +Status and License +------------------ - tox -e py +``virtualenv`` is a successor to `workingenv +`_, and an extension +of `virtual-python +`_. -This will run the test suite for the same Python version as under which ``tox`` is installed. Alternatively you can -specify a specific version of python by using the ``pyNN`` format, such as: ``py38``, ``pypy3``, etc. - -``tox`` has been configured to forward any additional arguments it is given to ``pytest``. This enables the use of -pytest's `rich CLI `_. As an example, you -can select tests using the various ways that pytest provides: - -.. code-block:: console - - # Using markers - tox -e py -- -m "not slow" - # Using keywords - tox -e py -- -k "test_extra" - -Some tests require additional dependencies to be run, such is the various shell activators (``bash``, ``fish``, -``powershell``, etc). These tests will automatically be skipped if these are not present, note however that in CI all -tests are run; so even if all tests succeed locally for you, they may still fail in the CI. - -Running linters -=============== - -virtualenv uses :pypi:`pre-commit` for managing linting of the codebase. ``pre-commit`` performs various checks on all -files in virtualenv and uses tools that help follow a consistent code style within the codebase. To use linters locally, -run: - -.. code-block:: console - - tox -e fix - -.. note:: - - Avoid using ``# noqa`` comments to suppress linter warnings - wherever possible, warnings should be fixed instead. - ``# noqa`` comments are reserved for rare cases where the recommended style causes severe readability problems. - -Type checking -============= - -virtualenv ships a :PEP:`561` ``py.typed`` marker and has comprehensive type annotations across the entire codebase. -This means downstream consumers and type checkers automatically recognize virtualenv as an inline-typed package. - -All new code **must** include complete type annotations for function parameters and return types. To verify annotations -locally, run: - -.. code-block:: console - - tox -e type - -This uses `ty `_ (Astral's Rust-based type checker) to validate annotations against Python -3.14. A second environment checks compatibility with the minimum supported version: - -.. code-block:: console - - tox -e type-3.8 - -Both environments validate that annotations are consistent and correct. - -Annotation guidelines ---------------------- - -- Use ``from __future__ import annotations`` at the top of every module (enforced by ruff's ``required-imports`` - setting). -- Place imports that are only needed for type checking inside an ``if TYPE_CHECKING:`` block to avoid runtime overhead. -- Ruff's ``ANN`` rules are enabled. ``ANN401`` (``typing.Any``) is suppressed on a case-by-case basis with inline ``# - noqa: ANN401`` comments where ``Any`` is genuinely required (e.g. serialization, dynamic dispatch). -- Prefer concrete types over ``Any``. Use ``Union`` / ``|`` for nullable or multi-type parameters. -- When a type error is genuinely unfixable (e.g. third-party library limitations), suppress it with an inline ``# ty: - ignore[rule-name]`` comment and a brief justification. - -Building documentation -====================== - -virtualenv's documentation is built using :pypi:`Sphinx`. The documentation is written in reStructuredText. To build it -locally, run: - -.. code-block:: console - - tox -e docs - -The built documentation can be found in the ``.tox/docs_out`` folder and may be viewed by opening ``index.html`` within -that folder. - -Release -======= - -virtualenv's release schedule is tied to ``pip`` and ``setuptools``. We bundle the latest version of these libraries so -each time there's a new version of any of these, there will be a new virtualenv release shortly afterwards (we usually -wait just a few days to avoid pulling in any broken releases). - -Performing a release --------------------- - -A full release publishes to `PyPI `_, creates a `GitHub Release -`_ with the zipapp attached, and updates `get-virtualenv -`_ so that ``https://bootstrap.pypa.io/virtualenv.pyz`` serves the new version. - -Version bumping -^^^^^^^^^^^^^^^ - -The ``--version`` argument to ``tox r -e release`` controls the version. It defaults to ``auto``, which inspects the -``docs/changelog`` directory: if any ``*.feature.rst`` or ``*.removal.rst`` fragments exist, the minor version is -bumped, otherwise the patch version is bumped. You can also pass ``major``, ``minor``, or ``patch`` explicitly. - -Both methods produce identical results: a release commit and tag on ``main``. Pushing the tag triggers the `Release -workflow `_ which builds the sdist, wheel, and -zipapp, publishes to PyPI via trusted publisher, creates a `GitHub Release -`_ with the zipapp attached, and updates `get-virtualenv -`_. If publish fails, a rollback job automatically reverts everything. - -**Via GitHub Actions (recommended)** - -1. Go to the `Pre-release workflow `_ on GitHub. -2. Click **Run workflow** and select the bump type (``auto``, ``major``, ``minor``, or ``patch``). - -**Locally** - -.. code-block:: console - - tox r -e release - -Pass ``--version `` to override the default ``auto`` behavior (e.g. ``--version minor``). - -************** - Contributing -************** - -Submitting pull requests -======================== - -Submit pull requests against the ``main`` branch, providing a good description of what you're doing and why. You must -have legal permission to distribute any code you contribute to virtualenv and it must be available under the MIT -License. Provide tests that cover your changes and run the tests locally first. virtualenv :ref:`supports -` multiple Python versions and operating systems. Any pull request must consider and work on -all these platforms. - -Pull Requests should be small to facilitate review. Keep them self-contained, and limited in scope. `Studies have shown -`_ that review quality falls off as patch size -grows. Sometimes this will result in many small PRs to land a single large feature. In particular, pull requests must -not be treated as "feature branches", with ongoing development work happening within the PR. Instead, the feature should -be broken up into smaller, independent parts which can be reviewed and merged individually. - -Additionally, avoid including "cosmetic" changes to code that is unrelated to your change, as these make reviewing the -PR more difficult. Examples include re-flowing text in comments or documentation, or addition or removal of blank lines -or whitespace within lines. Such changes can be made separately, as a "formatting cleanup" PR, if needed. - -Automated testing -================= - -All pull requests and merges to 'main' branch are tested using `GitHub Actions `_ -(configured by ``.github/workflows/check.yaml`` file at the root of the repository). You can find the status and results -to the CI runs for your PR on GitHub's Web UI for the pull request. You can also find links to the CI services' pages -for the specific builds in the form of "Details" links, in case the CI run fails and you wish to view the output. - -To trigger CI to run again for a pull request, you can close and open the pull request or submit another change to the -pull request. If needed, project maintainers can manually trigger a restart of a job/build. - -NEWS entries -============ - -The ``changelog.rst`` file is managed using :pypi:`towncrier` and all non trivial changes must be accompanied by a news -entry. To add an entry to the news file, first you need to have created an issue describing the change you want to make. -A Pull Request itself *may* function as such, but it is preferred to have a dedicated issue (for example, in case the PR -ends up rejected due to code quality reasons). - -Once you have an issue or pull request, you take the number and you create a file inside of the ``docs/changelog`` -directory named after that issue number with an extension of: - -- ``feature.rst``, -- ``bugfix.rst``, -- ``doc.rst``, -- ``removal.rst``, -- ``misc.rst``. - -Thus if your issue or PR number is ``1234`` and this change is fixing a bug, then you would create a file -``docs/changelog/1234.bugfix.rst``. PRs can span multiple categories by creating multiple files (for instance, if you -added a feature and deprecated/removed the old feature at the same time, you would create -``docs/changelog/1234.bugfix.rst`` and ``docs/changelog/1234.remove.rst``). Likewise if a PR touches multiple issues/PRs -you may create a file for each of them with the same contents and :pypi:`towncrier` will deduplicate them. - -Contents of a NEWS entry ------------------------- - -The contents of this file are reStructuredText formatted text that will be used as the content of the news file entry. -You do not need to reference the issue or PR numbers here as towncrier will automatically add a reference to all of the -affected issues when rendering the news file. - -In order to maintain a consistent style in the ``changelog.rst`` file, it is preferred to keep the news entry to the -point, in sentence case, shorter than 120 characters and in an imperative tone -- an entry should complete the sentence -``This change will โ€ฆ``. In rare cases, where one line is not enough, use a summary line in an imperative tone followed -by a blank line separating it from a description of the feature/change in one or more paragraphs, each wrapped at 120 -characters. Remember that a news entry is meant for end users and should only contain details relevant to an end user. - -Choosing the type of NEWS entry -------------------------------- - -A trivial change is anything that does not warrant an entry in the news file. Some examples are: code refactors that -don't change anything as far as the public is concerned, typo fixes, white space modification, etc. To mark a PR as -trivial a contributor simply needs to add a randomly named, empty file to the ``news/`` directory with the extension of -``.trivial``. - -Becoming a maintainer -===================== - -If you want to become an official maintainer, start by helping out. As a first step, we welcome you to triage issues on -virtualenv's issue tracker. virtualenv maintainers provide triage abilities to contributors once they have been around -for some time and contributed positively to the project. This is optional and highly recommended for becoming a -virtualenv maintainer. Later, when you think you're ready, get in touch with one of the maintainers and they will -initiate a vote among the existing maintainers. - -.. note:: - - Upon becoming a maintainer, a person should be given access to various virtualenv-related tooling across multiple - platforms. These are noted here for future reference by the maintainers: - - - GitHub Push Access - - PyPI Publishing Access - - CI Administration capabilities - - ReadTheDocs Administration capabilities +It was written by Ian Bicking, sponsored by the `Open Planning +Project `_ and is now maintained by a +`group of developers `_. +It is licensed under an +`MIT-style permissive license `_. diff --git a/docs/explanation.rst b/docs/explanation.rst deleted file mode 100644 index 86229a103..000000000 --- a/docs/explanation.rst +++ /dev/null @@ -1,512 +0,0 @@ -############# - Explanation -############# - -This page explains the design decisions and concepts behind virtualenv. It focuses on understanding why things work the -way they do. - -************************** - virtualenv vs venv vs uv -************************** - -Since Python 3.3, the standard library includes the ``venv`` module, which provides basic virtual environment creation -following `PEP 405 `_. `uv `_ is -a newer, Rust-based tool that also creates virtual environments via ``uv venv``. - -virtualenv occupies a middle ground: faster and more featureful than ``venv``, while remaining a pure Python solution -with a plugin system for extensibility. - -.. list-table:: - :header-rows: 1 - :widths: 20 27 27 26 - - - - - - ``venv`` - - ``virtualenv`` - - `uv `_ - - - Performance - - Slowest (60s+); spawns `pip `_ as a subprocess to seed. - - Fast; caches pre-built install images, subsequent creation < 1 second. - - Fastest; Rust implementation, milliseconds. Does not seed pip/setuptools by default. - - - Extensibility - - No plugin system. - - Plugin system for discovery, creation, seeding, and activation. - - No plugin system. - - - Cross-version - - Only the Python version it runs under. - - Any installed Python via auto-discovery (registry, uv-managed, PATH). - - Any installed or uv-managed Python. - - - Upgradeability - - Tied to Python releases. - - Independent via `PyPI `_. - - Independent via its own release cycle. - - - Programmatic API - - Basic ``create()`` function only. - - Full Python API; can describe environments without creating them. Used by `tox `_, - `poetry `_, `pipx `_, etc. - - Command line only. - - - Type annotations - - No ``py.typed`` marker; limited annotations. - - Fully typed with :PEP:`561` ``py.typed`` marker; checked by `ty `_. - - Not applicable (Rust binary). - - - Best for - - Zero dependencies, basic needs. - - Plugin extensibility, programmatic API, tool compatibility (`tox `_, - `virtualenvwrapper `_). - - Maximum speed, already using ``uv`` for package management. - -.. mermaid:: - - flowchart TD - A{Need plugins or programmatic API?} -->|Yes| V[virtualenv] - A -->|No| B{Already using uv?} - B -->|Yes| U[uv venv] - B -->|No| C{Can install external tools?} - C -->|Yes| D{Speed matters?} - C -->|No| VENV[venv] - D -->|Yes| U - D -->|No| V - - style A fill:#d97706,stroke:#b45309,color:#fff - style B fill:#d97706,stroke:#b45309,color:#fff - style C fill:#d97706,stroke:#b45309,color:#fff - style D fill:#d97706,stroke:#b45309,color:#fff - style V fill:#16a34a,stroke:#15803d,color:#fff - style U fill:#2563eb,stroke:#1d4ed8,color:#fff - style VENV fill:#7c3aed,stroke:#6d28d9,color:#fff - -********************** - How virtualenv works -********************** - -Python packaging often faces a fundamental problem: different applications require different versions of the same -library. If Application A needs ``requests==2.25.1`` but Application B needs ``requests==2.28.0``, installing both into -the global site-packages directory creates a conflict. Only one version can exist in a given location. - -virtualenv solves this by creating isolated Python environments. Each environment has its own installation directories -and can maintain its own set of installed packages, independent of other environments and the system Python. - -virtualenv operates in two distinct phases: - -.. mermaid:: - - flowchart TD - Start([virtualenv command]) --> Phase1[Phase 1: Python Discovery] - Phase1 --> Discover{Find Python interpreter} - Discover -->|Default| SameVersion[Use virtualenv's own Python] - Discover -->|--python flag| CustomVersion[Use specified Python] - CustomVersion --> Phase2[Phase 2: Environment Creation] - SameVersion --> Phase2 - Phase2 --> CreatePython[Create Python matching target interpreter] - CreatePython --> SeedPackages[Install seed packages: pip, setuptools, wheel] - SeedPackages --> ActivationScripts[Install activation scripts] - ActivationScripts --> VCSIgnore[Create VCS ignore files] - VCSIgnore --> Complete([Virtual environment ready]) - - style Start fill:#2563eb,stroke:#1d4ed8,color:#fff - style Phase1 fill:#6366f1,stroke:#4f46e5,color:#fff - style Phase2 fill:#6366f1,stroke:#4f46e5,color:#fff - style Complete fill:#16a34a,stroke:#15803d,color:#fff - style Discover fill:#d97706,stroke:#b45309,color:#fff - -**Phase 1: Discover a Python interpreter** - virtualenv first identifies which Python interpreter to use as the template for the virtual environment. By default, - it uses the same Python version that virtualenv itself is running on. You can override this with the ``--python`` - flag to specify a different interpreter. - -**Phase 2: Create the virtual environment** - Once the target interpreter is identified, virtualenv creates the environment in four steps: - - 1. Create a Python executable matching the target interpreter - 2. Install seed packages (pip, setuptools, wheel) to enable package installation - 3. Install activation scripts for various shells - 4. Create VCS ignore files (currently Git's ``.gitignore``, skip with ``--no-vcs-ignore``) - -An important design principle: virtual environments are not self-contained. A complete Python installation consists of -thousands of files, and copying all of them into every virtual environment would be wasteful. Instead, virtual -environments are lightweight shells that borrow most content from the system Python. They contain only what's needed to -redirect Python's behavior. - -This design has two implications: - -- Environment creation is fast because only a small number of files need to be created. -- Upgrading the system Python might affect existing virtual environments, since they reference the system Python's - standard library and binary extensions. - -The Python executable in a virtual environment is effectively isolated from the one used to create it, but the -supporting files are shared. - -.. warning:: - - If you upgrade your system Python, existing virtual environments will still report the old version (the version - number is embedded in the Python executable itself), but they will use the new version's standard library and binary - extensions. This normally works without issues, but be aware that the environment is effectively running a hybrid of - old and new Python versions. - -****************** - Python discovery -****************** - -Before creating a virtual environment, virtualenv must locate a Python interpreter. The interpreter determines the -virtual environment's Python version, implementation (CPython, PyPy, etc.), and architecture (32-bit or 64-bit). - -The ``--python`` flag accepts several specifier formats: - -**Path specifier** - An absolute or relative path to a Python executable, such as ``/usr/bin/python3.8`` or ``./python``. - -**Version specifier** - A string following the format ``{implementation}{version}{architecture}{machine}`` where: - - - Implementation is alphabetic characters (``python`` means any implementation; if omitted, defaults to ``python``). - - Version is dot-separated numbers, optionally followed by ``t`` for free-threading builds. - - Architecture is ``-64`` or ``-32`` (if omitted, means any architecture). - - Machine is the CPU instruction set architecture, e.g. ``-arm64``, ``-x86_64``, ``-aarch64`` (if omitted, means any - machine). Cross-platform aliases are normalized automatically (``amd64`` โ†” ``x86_64``, ``aarch64`` โ†” ``arm64``). - - Examples: - - - ``python3.8.1`` - Any Python implementation with version 3.8.1 - - ``3`` - Any Python implementation with major version 3 - - ``3.13t`` - Any Python implementation version 3.13 with free-threading enabled - - ``cpython3`` - CPython implementation with major version 3 - - ``pypy2`` - PyPy implementation with major version 2 - - ``cpython3.12-64-arm64`` - CPython 3.12, 64-bit, ARM64 architecture - - ``3.11-64-x86_64`` - Any implementation, version 3.11, 64-bit, x86_64 architecture - - ``rustpython`` - RustPython implementation - -**PEP 440 version specifier** - Version constraints using PEP 440 operators: - - - ``>=3.12`` - Any Python 3.12 or later - - ``~=3.11.0`` - Compatible with Python 3.11.0 - - ``cpython>=3.10`` - CPython 3.10 or later - -When you provide a specifier, virtualenv searches for matching interpreters using this strategy: - -.. mermaid:: - - flowchart TD - Start([Python specifier provided]) --> Windows{Running on Windows?} - Windows -->|Yes| Registry[Check Windows Registry per PEP-514] - Windows -->|No| UVManaged - Registry --> RegistryMatch{Match found?} - RegistryMatch -->|Yes| Found([Use matched Python]) - RegistryMatch -->|No| UVManaged[Check uv-managed Python installations] - UVManaged --> UVMatch{Match found?} - UVMatch -->|Yes| Found - UVMatch -->|No| PATH[Search PATH for matching executable] - PATH --> PATHMatch{Match found?} - PATHMatch -->|Yes| Found - PATHMatch -->|No| NotFound([Discovery fails]) - - style Start fill:#2563eb,stroke:#1d4ed8,color:#fff - style Found fill:#16a34a,stroke:#15803d,color:#fff - style NotFound fill:#dc2626,stroke:#b91c1c,color:#fff - style Windows fill:#d97706,stroke:#b45309,color:#fff - style RegistryMatch fill:#d97706,stroke:#b45309,color:#fff - style UVMatch fill:#d97706,stroke:#b45309,color:#fff - style PATHMatch fill:#d97706,stroke:#b45309,color:#fff - -1. **Windows Registry** (Windows only): Check registered Python installations per `PEP 514 - `_. -2. **uv-managed installations**: Check the ``UV_PYTHON_INSTALL_DIR`` environment variable or platform-specific uv Python - directories for managed Python installations. -3. **PATH search**: Search for executables on the ``PATH`` environment variable with names matching the specification. - -Version manager shim resolution -=============================== - -Version managers like `pyenv `_, `mise `_, and `asdf -`_ place lightweight shim scripts on ``PATH`` that delegate to the real Python binary. When -virtualenv discovers a Python interpreter by running it as a subprocess, shims may resolve to the wrong Python version -(typically the system Python) because the shim's resolution logic depends on shell environment state that doesn't fully -propagate to child processes. - -virtualenv detects shims by checking whether the candidate executable lives in a known shim directory -(``$PYENV_ROOT/shims``, ``$MISE_DATA_DIR/shims``, or ``$ASDF_DATA_DIR/shims``). When a shim is detected, virtualenv -bypasses it and locates the real binary directly under the version manager's ``versions`` directory, using the active -version from: - -1. The ``PYENV_VERSION`` environment variable (colon-separated for multiple versions). -2. A ``.python-version`` file in the current directory or any parent directory. -3. The global version file at ``$PYENV_ROOT/version``. - -This convention is shared across pyenv, mise, and asdf, so the same resolution logic works for all three. - -.. warning:: - - Virtual environments typically reference the system Python's standard library. If you upgrade the system Python, the - virtual environment will report the old version (embedded in its Python executable) but will actually use the new - version's standard library content. This can cause confusion when debugging version-specific behavior. - - If you use a virtual environment's Python as the target for creating another virtual environment, virtualenv will - detect the system Python version and create an environment matching the actual (upgraded) version, not the version - reported by the virtual environment. - -********** - Creators -********** - -Creators are responsible for constructing the virtual environment structure. virtualenv supports two types of creators: - -**venv creator** - This creator delegates the entire creation process to the standard library's ``venv`` module, following `PEP 405 - `_. The venv creator has two limitations: - - - It only works with Python 3.5 or later. - - It requires spawning a subprocess to invoke the venv module, unless virtualenv is installed in the system Python. - - The subprocess overhead can be significant, especially on Windows where process creation is expensive. - -**builtin creator** - This creator means virtualenv performs the creation itself by knowing exactly which files to create and which system - files to reference. The builtin creator is actually a family of specialized creators for different combinations of - Python implementation (CPython, PyPy, GraalPy, RustPython) and platform (Windows, POSIX). The name ``builtin`` is an - alias that selects the first available builtin creator for the target environment. - - Because builtin creators don't require subprocess invocation, they're generally faster than the venv creator. - -.. mermaid:: - - flowchart TD - Start([Select creator]) --> Builtin{Builtin creator available?} - Builtin -->|Yes| UseBuiltin([Use builtin creator - faster, no subprocess]) - Builtin -->|No| UseVenv([Use venv creator - delegates to stdlib]) - - style Start fill:#2563eb,stroke:#1d4ed8,color:#fff - style UseBuiltin fill:#16a34a,stroke:#15803d,color:#fff - style UseVenv fill:#7c3aed,stroke:#6d28d9,color:#fff - style Builtin fill:#d97706,stroke:#b45309,color:#fff - -virtualenv defaults to using the builtin creator if one is available for the target environment, falling back to the -venv creator otherwise. - -********* - Seeders -********* - -After creating the virtual environment structure, virtualenv installs seed packages that enable package management -within the environment. The seed packages are: - -- ``pip`` - The package installer for Python (always installed). -- ``setuptools`` - Package development and installation library (disabled by default on Python 3.12+). -- ``wheel`` - Support for the wheel binary package format (only installed by default on Python 3.8). - -virtualenv supports two seeding methods with dramatically different performance characteristics: - -**pip seeder** - This method uses the bundled pip wheel to install seed packages by spawning a child pip process. The subprocess - performs a full installation, including unpacking wheels and generating metadata. This method is reliable but slow, - typically consuming 98% of the total virtual environment creation time. - -**app-data seeder** - This method creates reusable install images in a user application data directory. The first time you create an - environment with specific seed package versions, the app-data seeder builds complete install images and stores them - in the cache. Subsequent environment creations simply link or copy these pre-built images into the virtual - environment's ``site-packages`` directory. - - Performance comparison for creating virtual environments: - - .. mermaid:: - - xychart-beta horizontal - title "Seeding time (seconds, lower is better)" - x-axis ["pip seeder (70s)", "app-data copy Win (8s)", "app-data symlink Win (0.8s)", "app-data symlink Linux/macOS (0.1s)"] - y-axis "Seconds" 0 --> 80 - bar [70, 8, 0.8, 0.1] - - On platforms that support symlinks efficiently (Linux, macOS), the app-data seeder provides nearly instant seeding. - - You can override the cache location using the ``VIRTUALENV_OVERRIDE_APP_DATA`` environment variable. - -.. _wheels: - -Wheel acquisition -================= - -Both seeding methods require wheel files for the seed packages. virtualenv acquires wheels using a priority system: - -.. mermaid:: - - flowchart TD - Start([Need wheel file]) --> Embedded{Found in embedded wheels?} - Embedded -->|Yes| UseEmbedded([Use embedded wheel]) - Embedded -->|No| Upgraded{Found in upgraded wheels?} - Upgraded -->|Yes| UseUpgraded([Use upgraded wheel]) - Upgraded -->|No| Extra{Found in extra-search-dir?} - Extra -->|Yes| UseExtra([Use extra wheel]) - Extra -->|No| Download{Download enabled?} - Download -->|Yes| DownloadPyPI([Download from PyPI]) - Download -->|No| Fail([Seeding fails]) - - style Start fill:#2563eb,stroke:#1d4ed8,color:#fff - style UseEmbedded fill:#16a34a,stroke:#15803d,color:#fff - style UseUpgraded fill:#16a34a,stroke:#15803d,color:#fff - style UseExtra fill:#16a34a,stroke:#15803d,color:#fff - style DownloadPyPI fill:#16a34a,stroke:#15803d,color:#fff - style Fail fill:#dc2626,stroke:#b91c1c,color:#fff - style Embedded fill:#d97706,stroke:#b45309,color:#fff - style Upgraded fill:#d97706,stroke:#b45309,color:#fff - style Extra fill:#d97706,stroke:#b45309,color:#fff - style Download fill:#d97706,stroke:#b45309,color:#fff - -**Embedded wheels** - virtualenv ships with a set of wheels bundled directly into the package. These are tested with the virtualenv - release and provide a baseline set of seed packages. Different Python versions require different package versions, - so virtualenv bundles multiple wheels to support its wide Python version range. - -**Upgraded embedded wheels** - Users can manually upgrade the embedded wheels by running virtualenv with the ``--upgrade-embed-wheels`` flag. This - fetches newer versions of seed packages from PyPI and stores them in the user application data directory. Subsequent - virtualenv invocations will use these upgraded wheels instead of the embedded ones. - - virtualenv can also perform periodic automatic upgrades (see below). - -**Extra search directories** - Users can specify additional directories containing wheels using the ``--extra-search-dir`` flag. This is useful in - air-gapped environments or when using custom package builds. - -**PyPI download** - If no suitable wheel is found in the above locations, or if the ``--download`` flag is set, virtualenv will use pip - to download the latest compatible version from PyPI. - -Periodic update mechanism -========================= - -To keep the seed packages reasonably current without requiring users to manually upgrade virtualenv or run -``--upgrade-embed-wheels``, virtualenv implements a periodic automatic update system: - -.. mermaid:: - - timeline - title Periodic update safety gates - section PyPI release - Package published : New wheel available on PyPI - section 28-day wait - Day 1-28 : Wheel is too new, ignored by virtualenv - section Check interval - Every 14 days : virtualenv checks for eligible wheels - section 1-hour hold - After download : Wheel downloaded but not yet used - +1 hour : Wheel becomes active for new environments - -The 28-day waiting period protects users from automatically adopting newly released packages that might contain bugs. -The 1-hour delay after download ensures continuous integration systems don't start using different package versions -mid-run, which could cause confusing test failures. - -You can disable the periodic update mechanism with the ``--no-periodic-update`` flag. - -.. _distribution_wheels: - -Distribution maintainer patching -================================ - -Operating system distributions and package managers sometimes need to customize which seed package versions virtualenv -uses. They want to align virtualenv's bundled packages with system package versions. - -Distributions can patch the ``virtualenv.seed.wheels.embed`` module, replacing the ``get_embed_wheel`` function with -their own implementation that returns distribution-provided wheels. If they want to use virtualenv's test suite for -validation, they should also provide the ``BUNDLE_FOLDER``, ``BUNDLE_SUPPORT``, and ``MAX`` variables. - -Distributions should also consider patching ``virtualenv.seed.embed.base_embed.PERIODIC_UPDATE_ON_BY_DEFAULT`` to -``False``, allowing the system package manager to control seed package updates rather than virtualenv's periodic update -mechanism. Users can still manually request upgrades via ``--upgrade-embed-wheels``, but automatic updates won't -interfere with system-managed packages. - -************ - Activators -************ - -Activation scripts modify the current shell environment to prioritize the virtual environment's executables. This is -purely a convenience mechanism - you can always use absolute paths to virtual environment executables without -activating. - -What activation does: - -.. mermaid:: - - flowchart TD - Before([Before activation]) --> ModifyPATH[Prepend venv/bin to PATH] - ModifyPATH --> SetVENV[Set VIRTUAL_ENV variable] - SetVENV --> SetPROMPT[Set VIRTUAL_ENV_PROMPT variable] - SetPROMPT --> SetPKG[Set PKG_CONFIG_PATH] - SetPKG --> ModifyPrompt[Modify shell prompt] - ModifyPrompt --> After([After activation]) - - style Before fill:#2563eb,stroke:#1d4ed8,color:#fff - style After fill:#16a34a,stroke:#15803d,color:#fff - style ModifyPATH fill:#6366f1,stroke:#4f46e5,color:#fff - style SetVENV fill:#6366f1,stroke:#4f46e5,color:#fff - style SetPROMPT fill:#6366f1,stroke:#4f46e5,color:#fff - style SetPKG fill:#6366f1,stroke:#4f46e5,color:#fff - style ModifyPrompt fill:#6366f1,stroke:#4f46e5,color:#fff - -**PATH modification** - The activation script prepends the virtual environment's ``bin`` directory (``Scripts`` on Windows) to the ``PATH`` - environment variable. This ensures that when you run ``python``, ``pip``, or other executables, the shell finds the - virtual environment's versions first. - -**Environment variables** - Activation sets several environment variables: - - - ``VIRTUAL_ENV`` - Absolute path to the virtual environment directory. - - ``VIRTUAL_ENV_PROMPT`` - The prompt prefix (the environment name or custom value from ``--prompt``). - - ``PKG_CONFIG_PATH`` - Modified to include the virtual environment's ``lib/pkgconfig`` directory. - -**Prompt modification** - By default, activation prepends the environment name to your shell prompt, typically shown as ``(venv)`` before the - regular prompt. This visual indicator helps you remember which environment is active. You can customize this with - the ``--prompt`` flag when creating the environment, or disable it entirely by setting the - ``VIRTUAL_ENV_DISABLE_PROMPT`` environment variable. - -**Deactivation** - Activation scripts also provide a ``deactivate`` command that reverses the changes, restoring your original PATH and - removing the environment variables and prompt modifications. - -virtualenv provides activation scripts for multiple shells: - -- `Bash `_ (``activate``) -- `Fish `_ (``activate.fish``) -- `Csh/Tcsh `_ (``activate.csh``) -- `PowerShell `_ (``activate.ps1``) -- `Windows Batch `_ - (``activate.bat``) -- `Nushell `_ (``activate.nu``) -- Python (``activate_this.py``) -- for programmatic activation from within a running Python process, see - :ref:`how-to/usage:Programmatic activation` - -.. note:: - - On Windows 7 and later, PowerShell's default execution policy is ``Restricted``, which prevents running the - ``activate.ps1`` script. You can allow locally-generated scripts to run by changing the execution policy: - - .. code-block:: powershell - - Set-ExecutionPolicy RemoteSigned - - Since virtualenv generates ``activate.ps1`` locally for each environment, PowerShell considers it a local script - rather than a remote one and allows execution under the ``RemoteSigned`` policy. - -Remember: activation is optional. The following commands are equivalent: - -.. code-block:: console - - # With activation - source venv/bin/activate - python script.py - deactivate - - # Without activation - venv/bin/python script.py - -For a deeper dive into how activation works under the hood, see Allison Kaptur's blog post `There's no magic: virtualenv -edition `_, which explains how virtualenv uses -``PATH`` and ``PYTHONHOME`` to isolate virtual environments. - -********** - See also -********** - -- :doc:`how-to/usage` - Practical guides for common virtualenv tasks. -- :doc:`reference/cli` - Complete CLI reference documentation. diff --git a/docs/how-to/install.rst b/docs/how-to/install.rst deleted file mode 100644 index 78ad552c5..000000000 --- a/docs/how-to/install.rst +++ /dev/null @@ -1,103 +0,0 @@ -#################### - Install virtualenv -#################### - -virtualenv is a command-line tool, so it should be installed in an isolated environment rather than into your system -Python. Pick the method that fits your setup: - -- `uv `_ -- fast, modern Python package manager. Use this if you already have ``uv`` or are - starting fresh. -- `pipx `_ -- installs Python CLI tools in isolated environments. Use this if you already - have ``pipx`` set up. -- `pip `_ -- the standard Python package installer. Use ``--user`` to avoid modifying - system packages. May not work on distributions with externally-managed Python environments. -- `zipapp `_ -- a self-contained executable requiring no installation. - Use this in CI or environments where you cannot install packages. - -.. mermaid:: - - flowchart TD - A{Can you install packages?} -->|Yes| B{Have uv?} - A -->|No| Z[zipapp] - B -->|Yes| U[uv tool install] - B -->|No| C{Have pipx?} - C -->|Yes| P[pipx install] - C -->|No| D[pip install --user] - - style A fill:#d97706,stroke:#b45309,color:#fff - style B fill:#d97706,stroke:#b45309,color:#fff - style C fill:#d97706,stroke:#b45309,color:#fff - style U fill:#16a34a,stroke:#15803d,color:#fff - style P fill:#16a34a,stroke:#15803d,color:#fff - style D fill:#7c3aed,stroke:#6d28d9,color:#fff - style Z fill:#7c3aed,stroke:#6d28d9,color:#fff - -.. tab:: uv - - Install virtualenv as a `uv tool `_: - - .. code-block:: console - - $ uv tool install virtualenv - - Install the development version: - - .. code-block:: console - - $ uv tool install git+https://github.com/pypa/virtualenv.git@main - -.. tab:: pipx - - Install virtualenv using `pipx `_: - - .. code-block:: console - - $ pipx install virtualenv - - Install the development version: - - .. code-block:: console - - $ pipx install git+https://github.com/pypa/virtualenv.git@main - -.. tab:: pip - - Install virtualenv using `pip `_: - - .. code-block:: console - - $ python -m pip install --user virtualenv - - Install the development version: - - .. code-block:: console - - $ python -m pip install git+https://github.com/pypa/virtualenv.git@main - - .. warning:: - - Some Linux distributions use system-managed Python environments. If you encounter errors about externally-managed - environments, use ``uv tool`` or ``pipx`` instead. - -.. tab:: zipapp - - Download the zipapp file and run it directly: - - .. code-block:: console - - $ python virtualenv.pyz --help - - Download the latest version from https://bootstrap.pypa.io/virtualenv.pyz or a specific version from - ``https://bootstrap.pypa.io/virtualenv/x.y/virtualenv.pyz``. - -********************* - Verify installation -********************* - -Check the installed version: - -.. code-block:: console - - $ virtualenv --version - -See :doc:`../reference/compatibility` for supported Python versions. diff --git a/docs/how-to/usage.rst b/docs/how-to/usage.rst deleted file mode 100644 index 4969daa17..000000000 --- a/docs/how-to/usage.rst +++ /dev/null @@ -1,355 +0,0 @@ -################ - Use virtualenv -################ - -************************* - Select a Python version -************************* - -By default, virtualenv uses the same Python version it runs under. Override this with ``--python`` or ``-p``. - -Using version specifiers -======================== - -Specify a Python version by name or version number: - -.. code-block:: console - - $ virtualenv -p python3.8 venv - $ virtualenv -p 3.10 venv - $ virtualenv -p pypy3 venv - $ virtualenv -p rustpython venv - -Using PEP 440 specifiers -======================== - -Use `PEP 440 `_ version specifiers to match Python versions: - -.. code-block:: console - - $ virtualenv --python ">=3.12" venv - $ virtualenv --python "~=3.11.0" venv - $ virtualenv --python "cpython>=3.10" venv - -- ``>=3.12`` -- any Python 3.12 or later. -- ``~=3.11.0`` -- compatible release, equivalent to ``>=3.11.0, <3.12.0`` (any 3.11.x patch). -- ``cpython>=3.10`` -- restrict to CPython implementation, 3.10 or later. - -Using free-threading Python -=========================== - -Create an environment with `free-threading Python `_: - -.. code-block:: console - - $ virtualenv -p 3.13t venv - -Targeting a specific CPU architecture -===================================== - -On machines that support multiple architectures โ€” such as Apple Silicon (arm64 + x86_64 via Rosetta) or Windows on ARM โ€” -you can request a specific CPU architecture by appending it to the spec string: - -.. code-block:: console - - $ virtualenv -p cpython3.12-64-arm64 venv - $ virtualenv -p 3.11-64-x86_64 venv - -Cross-platform aliases are normalized automatically, so ``amd64`` and ``x86_64`` are treated as equivalent, as are -``aarch64`` and ``arm64``. If omitted, any architecture matches (preserving existing behavior). - -Using absolute paths -==================== - -Specify the full path to a Python interpreter: - -.. code-block:: console - - $ virtualenv -p /usr/bin/python3.9 venv - -Using ``--try-first-with`` -========================== - -Use ``--try-first-with`` to provide a hint about which Python to check first. Unlike ``--python``, this is a hint rather -than a rule. The interpreter at this path is checked first, but only used if it matches the ``--python`` constraint. - -.. code-block:: console - - $ virtualenv --python ">=3.10" --try-first-with /usr/bin/python3.9 venv - -In this example, /usr/bin/python3.9 is checked first but rejected because it does not satisfy the >=3.10 constraint. - -Using version managers (pyenv, mise, asdf) -========================================== - -virtualenv automatically resolves shims from `pyenv `_, `mise `_, -and `asdf `_ to the real Python binary. Set the active Python version using any of the standard -mechanisms and virtualenv will discover it: - -.. code-block:: console - - $ pyenv local 3.12.0 - $ virtualenv venv # uses pyenv's 3.12.0, not the system Python - - $ PYENV_VERSION=3.11.0 virtualenv venv # uses 3.11.0 - -This also works with mise and asdf: - -.. code-block:: console - - $ mise use python@3.12 - $ virtualenv venv - -No additional configuration is required. See :doc:`../explanation` for details on how shim resolution works. - -******************************** - Activate a virtual environment -******************************** - -Activate the environment to modify your shell's PATH and environment variables. - -.. tab:: Bash/Zsh - - .. code-block:: console - - $ source venv/bin/activate - -.. tab:: Fish - - .. code-block:: console - - $ source venv/bin/activate.fish - -.. tab:: PowerShell - - .. code-block:: console - - PS> .\venv\Scripts\Activate.ps1 - - .. note:: - - If you encounter an execution policy error, run ``Set-ExecutionPolicy RemoteSigned`` to allow local scripts. - -.. tab:: CMD - - .. code-block:: console - - > .\venv\Scripts\activate.bat - -.. tab:: Nushell - - .. code-block:: console - - $ overlay use venv/bin/activate.nu - -Deactivate the environment -========================== - -Exit the virtual environment: - -.. code-block:: console - - $ deactivate - -Use without activation -====================== - -Use the environment without activating it by calling executables with their full paths: - -.. code-block:: console - - $ venv/bin/python script.py - $ venv/bin/pip install package - -Customize prompt -================ - -Set a custom prompt prefix: - -.. code-block:: console - - $ virtualenv --prompt myproject venv - -Disable the prompt modification by setting the ``VIRTUAL_ENV_DISABLE_PROMPT`` environment variable. - -Access the prompt string via the ``VIRTUAL_ENV_PROMPT`` environment variable. - -Programmatic activation -======================= - -Activate the environment from within a running Python process using ``activate_this.py``. This modifies ``sys.path`` and -environment variables in the current process so that subsequent imports resolve from the virtual environment. - -.. code-block:: python - - import runpy - - runpy.run_path("venv/bin/activate_this.py") - -A common use case is web applications served by a system-wide WSGI server (such as mod_wsgi or uWSGI) that need to load -packages from a virtual environment: - -.. code-block:: python - - import runpy - from pathlib import Path - - runpy.run_path(str(Path("/var/www/myapp/venv/bin/activate_this.py"))) - - from myapp import create_app # noqa: E402 - - application = create_app() - -******************** - Configure defaults -******************** - -Use a configuration file to set default options for virtualenv. - -Configuration file location -=========================== - -The configuration file is named ``virtualenv.ini`` and located in the platformdirs app config directory. Run -``virtualenv --help`` to see the exact location for your system. - -Override the location with the ``VIRTUALENV_CONFIG_FILE`` environment variable. - -Configuration format -==================== - -Derive configuration keys from command-line options by stripping leading ``-`` and replacing remaining ``-`` with ``_``: - -.. code-block:: ini - - [virtualenv] - python = /opt/python-3.8/bin/python - -Multi-value options -=================== - -Specify multiple values on separate lines: - -.. code-block:: ini - - [virtualenv] - extra_search_dir = - /path/to/dists - /path/to/other/dists - -Environment variables -===================== - -Set options using environment variables with the ``VIRTUALENV_`` prefix and uppercase key names: - -.. code-block:: console - - $ export VIRTUALENV_PYTHON=/opt/python-3.8/bin/python - -For multi-value options, separate values with commas or newlines. - -Override app-data location -========================== - -Set the ``VIRTUALENV_OVERRIDE_APP_DATA`` environment variable to override the default app-data cache directory location. - -Configuration priority -====================== - -Options are resolved in this order (highest to lowest priority): - -.. mermaid:: - - block-beta - columns 1 - A["Command-line arguments (highest)"] - B["Environment variables"] - C["Configuration file"] - D["Default values (lowest)"] - - style A fill:#16a34a,stroke:#15803d,color:#fff - style B fill:#2563eb,stroke:#1d4ed8,color:#fff - style C fill:#d97706,stroke:#b45309,color:#fff - style D fill:#6366f1,stroke:#4f46e5,color:#fff - -*********************** - Control seed packages -*********************** - -Upgrade embedded wheels -======================= - -Update the embedded wheel files to the latest versions: - -.. code-block:: console - - $ virtualenv --upgrade-embed-wheels - -Provide custom wheels -===================== - -Use custom wheel files from a local directory: - -.. code-block:: console - - $ virtualenv --extra-search-dir /path/to/wheels venv - -Download latest from PyPI -========================= - -Download the latest versions of seed packages from PyPI: - -.. code-block:: console - - $ virtualenv --download venv - -Disable periodic updates -======================== - -Disable automatic periodic updates of seed packages: - -.. code-block:: console - - $ virtualenv --no-periodic-update venv - -For distribution maintainers -============================ - -Patch the ``virtualenv.seed.wheels.embed`` module and set ``PERIODIC_UPDATE_ON_BY_DEFAULT`` to ``False`` to disable -periodic updates by default. See :doc:`../explanation` for implementation details. - -********************** - Use from Python code -********************** - -Call virtualenv from Python code using the ``cli_run`` function: - -.. code-block:: python - - from virtualenv import cli_run - - cli_run(["venv"]) - -Pass options as list elements: - -.. code-block:: python - - cli_run(["-p", "python3.8", "--without-pip", "myenv"]) - -Use the returned session object to access environment details: - -.. code-block:: python - - result = cli_run(["venv"]) - print(result.creator.dest) # path to created environment - print(result.creator.exe) # path to python executable - -Use ``session_via_cli`` to describe the environment without creating it: - -.. code-block:: python - - from virtualenv import session_via_cli - - session = session_via_cli(["venv"]) - # inspect session.creator, session.seeder, session.activators - -See :doc:`../reference/api` for complete API documentation. diff --git a/docs/index.rst b/docs/index.rst index 050eed9f5..e745a87b7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,137 +1,137 @@ -############ - virtualenv -############ +Virtualenv +========== -.. image:: https://img.shields.io/pypi/v/virtualenv?style=flat-square - :target: https://pypi.org/project/virtualenv/#history - :alt: Latest version on PyPI +`Mailing list `_ | +`Issues `_ | +`Github `_ | +`PyPI `_ | +User IRC: #pypa +Dev IRC: #pypa-dev -.. image:: https://img.shields.io/pypi/implementation/virtualenv?style=flat-square - :alt: PyPI - Implementation +Introduction +------------ -.. image:: https://img.shields.io/pypi/pyversions/virtualenv?style=flat-square - :alt: PyPI - Python Version +``virtualenv`` is a tool to create isolated Python environments. -.. image:: https://readthedocs.org/projects/virtualenv/badge/?version=latest&style=flat-square - :target: https://virtualenv.pypa.io - :alt: Documentation status +The basic problem being addressed is one of dependencies and versions, +and indirectly permissions. Imagine you have an application that +needs version 1 of LibFoo, but another application requires version +2. How can you use both these applications? If you install +everything into ``/usr/lib/python2.7/site-packages`` (or whatever your +platform's standard location is), it's easy to end up in a situation +where you unintentionally upgrade an application that shouldn't be +upgraded. -.. image:: https://img.shields.io/discord/803025117553754132 - :target: https://discord.gg/pypa - :alt: Discord +Or more generally, what if you want to install an application *and +leave it be*? If an application works, any change in its libraries or +the versions of those libraries can break the application. -.. image:: https://img.shields.io/pypi/dm/virtualenv?style=flat-square - :target: https://pypistats.org/packages/virtualenv - :alt: PyPI - Downloads +Also, what if you can't install packages into the global +``site-packages`` directory? For instance, on a shared host. -.. image:: https://img.shields.io/pypi/l/virtualenv?style=flat-square - :target: https://opensource.org/licenses/MIT - :alt: PyPI - License +In all these cases, ``virtualenv`` can help you. It creates an +environment that has its own installation directories, that doesn't +share libraries with other virtualenv environments (and optionally +doesn't access the globally installed libraries either). -.. image:: https://img.shields.io/github/issues/pypa/virtualenv?style=flat-square - :target: https://github.com/pypa/virtualenv/issues - :alt: Open issues - -.. image:: https://img.shields.io/github/issues-pr/pypa/virtualenv?style=flat-square - :target: https://github.com/pypa/virtualenv/pulls - :alt: Open pull requests - -.. image:: https://img.shields.io/github/stars/pypa/virtualenv?style=flat-square - :target: https://pypistats.org/packages/virtualenv - :alt: Package popularity - -``virtualenv`` is a tool to create isolated Python environments. Since Python 3.3, a subset of it has been integrated -into the standard library under the ``venv`` module. For how ``virtualenv`` compares to the stdlib ``venv`` module, see -:doc:`explanation`. - -****************** - Quick navigation -****************** - -**Tutorials** - Learn by doing - -- :doc:`tutorial/getting-started` โ€” Create your first virtual environment and learn the basic workflow - -**How-to guides** - Solve specific problems - -- :doc:`how-to/install` โ€” Install virtualenv on your system -- :doc:`how-to/usage` โ€” Select Python versions, activate environments, configure defaults, and use from Python code - -**Reference** - Technical information - -- :doc:`reference/compatibility` โ€” Supported Python versions and operating systems -- :doc:`reference/cli` โ€” Command line options and flags -- :doc:`reference/api` โ€” Programmatic Python API reference - -**Explanation** - Understand the concepts - -- :doc:`explanation` โ€” How virtualenv works under the hood and why it exists - -**Extensions** - -- :doc:`plugin/index` โ€” Extend virtualenv with custom creators, seeders, and activators - -****************** - Related projects -****************** - -Several tools build on virtualenv to provide higher-level workflows: - -- `virtualenvwrapper `_ โ€” Shell wrapper for creating and managing - multiple virtualenvs -- `pew `_ โ€” Python Env Wrapper, a set of commands to manage multiple virtual - environments -- `tox `_ โ€” Automate testing across multiple Python versions -- `nox `_ โ€” Flexible test automation in Python - -******************** - External resources -******************** - -Learn more about virtualenv from these community resources: - -- `Corey Schafer's virtualenv tutorial `_ โ€” Video walkthrough for beginners -- `Bernat Gabor's status quo `_ โ€” Talk about the current state of Python - packaging -- `Carl Meyer's reverse-engineering `_ โ€” - Deep dive into how virtualenv works internally - -.. toctree:: - :hidden: - :caption: Tutorial - - tutorial/getting-started - -.. toctree:: - :hidden: - :caption: How-to guides - - how-to/install - how-to/usage - -.. toctree:: - :hidden: - :caption: Reference - - reference/compatibility - reference/cli - reference/api - -.. toctree:: - :hidden: - :caption: Explanation - - explanation +.. comment: split here .. toctree:: - :hidden: - :caption: Extend - - plugin/index - -.. toctree:: - :hidden: - :caption: Project - - development - changelog + :maxdepth: 2 + + installation + userguide + reference + development + changes + +.. warning:: + + Python bugfix releases 2.6.8, 2.7.3, 3.1.5 and 3.2.3 include a change that + will cause "import random" to fail with "cannot import name urandom" on any + virtualenv created on a Unix host with an earlier release of Python + 2.6/2.7/3.1/3.2, if the underlying system Python is upgraded. This is due to + the fact that a virtualenv uses the system Python's standard library but + contains its own copy of the Python interpreter, so an upgrade to the system + Python results in a mismatch between the version of the Python interpreter + and the version of the standard library. It can be fixed by removing + ``$ENV/bin/python`` and re-running virtualenv on the same target directory + with the upgraded Python. + +Other Documentation and Links +----------------------------- + +* `Blog announcement of virtualenv`__. + + .. __: http://blog.ianbicking.org/2007/10/10/workingenv-is-dead-long-live-virtualenv/ + +* James Gardner has written a tutorial on using `virtualenv with + Pylons + `_. + +* Chris Perkins created a `showmedo video including virtualenv + `_. + +* Doug Hellmann's `virtualenvwrapper`_ is a useful set of scripts to make + your workflow with many virtualenvs even easier. `His initial blog post on it`__. + He also wrote `an example of using virtualenv to try IPython`__. + + .. _virtualenvwrapper: https://pypi.python.org/pypi/virtualenvwrapper/ + .. __: https://doughellmann.com/blog/2008/05/01/virtualenvwrapper/ + .. __: https://doughellmann.com/blog/2008/02/01/ipython-and-virtualenv/ + +* `Pew`_ is another wrapper for virtualenv that makes use of a different + activation technique. + + .. _Pew: https://pypi.python.org/pypi/pew/ + +* `Using virtualenv with mod_wsgi + `_. + +* `virtualenv commands + `_ for some more + workflow-related tools around virtualenv. + +* PyCon US 2011 talk: `Reverse-engineering Ian Bicking's brain: inside pip and virtualenv + `_. + By the end of the talk, you'll have a good idea exactly how pip + and virtualenv do their magic, and where to go looking in the source + for particular behaviors or bug fixes. + +Compare & Contrast with Alternatives +------------------------------------ + +There are several alternatives that create isolated environments: + +* ``workingenv`` (which I do not suggest you use anymore) is the + predecessor to this library. It used the main Python interpreter, + but relied on setting ``$PYTHONPATH`` to activate the environment. + This causes problems when running Python scripts that aren't part of + the environment (e.g., a globally installed ``hg`` or ``bzr``). It + also conflicted a lot with Setuptools. + +* `virtual-python + `_ + is also a predecessor to this library. It uses only symlinks, so it + couldn't work on Windows. It also symlinks over the *entire* + standard library and global ``site-packages``. As a result, it + won't see new additions to the global ``site-packages``. + + This script only symlinks a small portion of the standard library + into the environment, and so on Windows it is feasible to simply + copy these files over. Also, it creates a new/empty + ``site-packages`` and also adds the global ``site-packages`` to the + path, so updates are tracked separately. This script also installs + Setuptools automatically, saving a step and avoiding the need for + network access. + +* `zc.buildout `_ doesn't + create an isolated Python environment in the same style, but + achieves similar results through a declarative config file that sets + up scripts with very particular packages. As a declarative system, + it is somewhat easier to repeat and manage, but more difficult to + experiment with. ``zc.buildout`` includes the ability to setup + non-Python systems (e.g., a database server or an Apache instance). + +I *strongly* recommend anyone doing application development or +deployment use one of these tools. diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 000000000..3006d7617 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,58 @@ +Installation +============ + +.. warning:: + + We advise installing virtualenv-1.9 or greater. Prior to version 1.9, the + pip included in virtualenv did not download from PyPI over SSL. + +.. warning:: + + When using pip to install virtualenv, we advise using pip 1.3 or greater. + Prior to version 1.3, pip did not download from PyPI over SSL. + +.. warning:: + + We advise against using easy_install to install virtualenv when using + setuptools < 0.9.7, because easy_install didn't download from PyPI over SSL + and was broken in some subtle ways. + +To install globally with `pip` (if you have pip 1.3 or greater installed globally): + +:: + + $ [sudo] pip install virtualenv + +Or to get the latest unreleased dev version: + +:: + + $ [sudo] pip install https://github.com/pypa/virtualenv/tarball/develop + + +To install version X.X globally from source: + +:: + + $ curl -O https://pypi.python.org/packages/source/v/virtualenv/virtualenv-X.X.tar.gz + $ tar xvfz virtualenv-X.X.tar.gz + $ cd virtualenv-X.X + $ [sudo] python setup.py install + + +To *use* locally from source: + +:: + + $ curl -O https://pypi.python.org/packages/source/v/virtualenv/virtualenv-X.X.tar.gz + $ tar xvfz virtualenv-X.X.tar.gz + $ cd virtualenv-X.X + $ python virtualenv.py myVE + +.. note:: + + The ``virtualenv.py`` script is *not* supported if run without the + necessary pip/setuptools/virtualenv distributions available locally. All + of the installation methods above include a ``virtualenv_support`` + directory alongside ``virtualenv.py`` which contains a complete set of + pip and setuptools distributions, and so are fully supported. diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 000000000..aa5c189fc --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,170 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-compressor.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-compressor.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/docs/plugin/api.rst b/docs/plugin/api.rst deleted file mode 100644 index dee75e40e..000000000 --- a/docs/plugin/api.rst +++ /dev/null @@ -1,79 +0,0 @@ -###################### - Plugin API reference -###################### - -This page documents the interfaces that plugins must implement. - -*********** - Discovery -*********** - -Discovery plugins locate Python interpreters for creating virtual environments. - -.. currentmodule:: virtualenv.discovery.discover - -.. autoclass:: Discover - :undoc-members: - :members: - -PythonInfo -========== - -Discovery plugins return a ``PythonInfo`` object describing the located interpreter. - -.. currentmodule:: virtualenv.discovery.py_info - -.. autoclass:: PythonInfo - :undoc-members: - :members: - -********** - App data -********** - -The application data interface used by plugins for caching. - -.. currentmodule:: virtualenv.app_data.base - -.. autoclass:: AppData - :members: - -********** - Creators -********** - -Creator plugins build the virtual environment directory structure and install the Python interpreter. - -.. currentmodule:: virtualenv.create.creator - -.. autoclass:: CreatorMeta - :members: - -.. autoclass:: Creator - :undoc-members: - :members: - :exclude-members: run, set_pyenv_cfg, debug_script, validate_dest, debug - -********* - Seeders -********* - -Seeder plugins install initial packages (like pip, setuptools, wheel) into the virtual environment. - -.. currentmodule:: virtualenv.seed.seeder - -.. autoclass:: Seeder - :undoc-members: - :members: - -************ - Activators -************ - -Activator plugins generate shell-specific activation scripts. - -.. currentmodule:: virtualenv.activation.activator - -.. autoclass:: Activator - :undoc-members: - :members: diff --git a/docs/plugin/architecture.rst b/docs/plugin/architecture.rst deleted file mode 100644 index 4ce911622..000000000 --- a/docs/plugin/architecture.rst +++ /dev/null @@ -1,125 +0,0 @@ -##################### - Plugin architecture -##################### - -This page explains how virtualenv's plugin system works internally. - -************** - Entry points -************** - -virtualenv uses Python entry points (``setuptools`` / ``importlib.metadata``) to discover plugins. Each plugin registers -under one of four entry point groups: - -- ``virtualenv.discovery`` -- ``virtualenv.create`` -- ``virtualenv.seed`` -- ``virtualenv.activate`` - -At startup, virtualenv loads all registered entry points from these groups and makes them available as CLI options. -Built-in implementations are registered in virtualenv's own ``pyproject.toml``, while third-party plugins register their -entry points in their own package metadata. - -When a package with virtualenv plugins is installed in the same environment as virtualenv, the plugins become -immediately available without additional configuration. - -****************** - Plugin lifecycle -****************** - -The following diagram shows how plugins are discovered and executed: - -.. mermaid:: - - sequenceDiagram - participant User - participant CLI - participant EntryPoints - participant Discovery - participant Creator - participant Seeder - participant Activator - - rect rgba(37, 99, 235, 0.15) - User->>CLI: virtualenv myenv - CLI->>EntryPoints: Load plugins from all groups - EntryPoints-->>CLI: Available plugins - CLI->>CLI: Build argument parser with plugin options - CLI->>User: Parse CLI arguments - User-->>CLI: Selected options - end - - rect rgba(22, 163, 106, 0.15) - CLI->>Discovery: Run selected discovery plugin - Discovery-->>CLI: PythonInfo - CLI->>Creator: Create environment with PythonInfo - Creator-->>CLI: Created environment - CLI->>Seeder: Seed packages into environment - Seeder-->>CLI: Seeded environment - CLI->>Activator: Generate activation scripts - Activator-->>CLI: Complete environment - end - -The lifecycle follows these stages: - -1. virtualenv starts and discovers all entry points from the four plugin groups -2. The CLI parser is built dynamically, incorporating options from all discovered plugins -3. User arguments are parsed to select which discovery, creator, seeder, and activator plugins to use -4. Selected plugins execute in sequence: discover โ†’ create โ†’ seed โ†’ activate -5. Each stage passes its output to the next stage - -************************ - Extension point design -************************ - -Each extension point follows a consistent pattern: - -Base abstract class - Each extension point defines a base abstract class (``Discover``, ``Creator``, ``Seeder``, ``Activator``) that - specifies the interface plugins must implement. - -Built-in implementations - virtualenv includes built-in implementations registered as entry points in its own ``pyproject.toml``. For example, - the built-in CPython creator is registered as ``cpython3-posix``. - -Third-party plugins - External packages implement the base interface and register their own entry points under the same group. When - installed, they appear alongside built-in options. - -CLI selection - Command-line flags (``--discovery``, ``--creator``, ``--seeder``, ``--activators``) allow users to select which - implementation to use. Multiple activators can be selected simultaneously. - -Parser integration - Each plugin can contribute CLI arguments through the ``add_parser_arguments`` classmethod. These arguments appear in - ``virtualenv --help`` and are available when the plugin is selected. - -********************** - How plugins interact -********************** - -Plugins execute in a pipeline where each stage depends on the previous one: - -Discovery โ†’ Creator - The discovery plugin produces a ``PythonInfo`` object describing the source Python interpreter. This object contains - metadata about the Python version, platform, paths, and capabilities. The creator plugin receives this - ``PythonInfo`` and uses it to determine how to build the virtual environment structure. - -Creator โ†’ Seeder - The creator plugin produces a ``Creator`` object representing the newly created virtual environment. This includes - paths to the environment's ``bin`` directory, site-packages, and Python executable. The seeder plugin uses these - paths to install packages. - -Seeder โ†’ Activator - After seeding completes, activator plugins use the ``Creator`` object to generate shell activation scripts. These - scripts reference the environment's bin directory and other paths to configure the shell environment. - -This pipeline ensures that each plugin has the information it needs from previous stages. The ``PythonInfo`` flows from -discovery to creator, and the ``Creator`` object flows from creator to both seeder and activators. - -Plugin isolation -================ - -Plugins within the same extension point do not interact with each other. Only one discovery and one creator plugin can -run per invocation, though multiple activators can run simultaneously. This isolation keeps plugins simple and focused -on their specific task. diff --git a/docs/plugin/how-to.rst b/docs/plugin/how-to.rst deleted file mode 100644 index 7c11a91f8..000000000 --- a/docs/plugin/how-to.rst +++ /dev/null @@ -1,210 +0,0 @@ -###################### - Plugin how-to guides -###################### - -This page provides task-oriented guides for creating each type of virtualenv plugin. - -*************************** - Create a discovery plugin -*************************** - -Discovery plugins locate Python interpreters. Register your plugin under the ``virtualenv.discovery`` entry point group. - -Implement the ``Discover`` interface: - -.. code-block:: python - - from virtualenv.discovery.discover import Discover - from virtualenv.discovery.py_info import PythonInfo - - - class CustomDiscovery(Discover): - @classmethod - def add_parser_arguments(cls, parser): - parser.add_argument("--custom-opt", help="custom discovery option") - - def __init__(self, options): - super().__init__(options) - self.custom_opt = options.custom_opt - - def run(self): - # Locate Python interpreter and return PythonInfo - python_exe = self._find_python() - return PythonInfo.from_exe(str(python_exe)) - - def _find_python(self): - # Implementation-specific logic - pass - -Register the entry point: - -.. code-block:: ini - - [virtualenv.discovery] - custom = your_package.discovery:CustomDiscovery - -************************* - Create a creator plugin -************************* - -Creator plugins build the virtual environment structure. Register under ``virtualenv.create``. - -Implement the ``Creator`` interface: - -.. code-block:: python - - from virtualenv.create.creator import Creator - - - class CustomCreator(Creator): - @classmethod - def add_parser_arguments(cls, parser, interpreter): - parser.add_argument("--custom-creator-opt", help="custom creator option") - - def __init__(self, options, interpreter): - super().__init__(options, interpreter) - self.custom_opt = options.custom_creator_opt - - def create(self): - # Create directory structure - self.bin_dir.mkdir(parents=True, exist_ok=True) - # Copy or symlink Python executable - self.install_python() - # Set up site-packages - self.install_site_packages() - # Write pyvenv.cfg - self.set_pyenv_cfg() - -Register the entry point using a naming pattern that matches platform and Python version: - -.. code-block:: ini - - [virtualenv.create] - cpython3-posix = virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Posix - cpython3-win = virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Windows - -************************ - Create a seeder plugin -************************ - -Seeder plugins install initial packages into the virtual environment. Register under ``virtualenv.seed``. - -Implement the ``Seeder`` interface: - -.. code-block:: python - - from virtualenv.seed.seeder import Seeder - - - class CustomSeeder(Seeder): - @classmethod - def add_parser_arguments(cls, parser, interpreter, app_data): - parser.add_argument("--custom-seed-opt", help="custom seeder option") - - def __init__(self, options, enabled, app_data): - super().__init__(options, enabled, app_data) - self.custom_opt = options.custom_seed_opt - - def run(self, creator): - # Install packages into creator.bin_dir / creator.script("pip") - self._install_packages(creator) - - def _install_packages(self, creator): - # Implementation-specific logic - pass - -Register the entry point: - -.. code-block:: ini - - [virtualenv.seed] - custom = your_package.seed:CustomSeeder - -**************************** - Create an activator plugin -**************************** - -Activator plugins generate shell activation scripts. Register under ``virtualenv.activate``. - -Implement the ``Activator`` interface: - -.. code-block:: python - - from virtualenv.activation.activator import Activator - - - class CustomShellActivator(Activator): - def generate(self, creator): - # Generate activation script content - script_content = self._render_template(creator) - # Write to activation directory - dest = creator.bin_dir / self.script_name - dest.write_text(script_content) - - def _render_template(self, creator): - # Return activation script content - return f""" - # Custom shell activation script - export VIRTUAL_ENV="{creator.dest}" - export PATH="{creator.bin_dir}:$PATH" - """ - - @property - def script_name(self): - return "activate.custom" - -Register the entry point: - -.. code-block:: ini - - [virtualenv.activate] - bash = virtualenv.activation.bash:BashActivator - fish = virtualenv.activation.fish:FishActivator - custom = your_package.activation:CustomShellActivator - -********************************* - Package and distribute a plugin -********************************* - -Use ``pyproject.toml`` to declare entry points: - -.. code-block:: toml - - [project] - name = "virtualenv-custom-plugin" - version = "1.0.0" - dependencies = ["virtualenv>=20.0.0"] - - [project.entry-points."virtualenv.discovery"] - custom = "virtualenv_custom.discovery:CustomDiscovery" - - [project.entry-points."virtualenv.create"] - custom-posix = "virtualenv_custom.creator:CustomCreator" - - [project.entry-points."virtualenv.seed"] - custom = "virtualenv_custom.seeder:CustomSeeder" - - [project.entry-points."virtualenv.activate"] - custom = "virtualenv_custom.activator:CustomActivator" - - [build-system] - requires = ["setuptools>=61"] - build-backend = "setuptools.build_meta" - -Install your plugin alongside virtualenv: - -.. code-block:: console - - $ pip install virtualenv-custom-plugin - -Or in development mode: - -.. code-block:: console - - $ pip install -e /path/to/virtualenv-custom-plugin - -Test your plugin by creating a virtual environment: - -.. code-block:: console - - $ virtualenv --discovery=custom --creator=custom-posix --seeder=custom --activators=custom test-env diff --git a/docs/plugin/index.rst b/docs/plugin/index.rst deleted file mode 100644 index 79b39f952..000000000 --- a/docs/plugin/index.rst +++ /dev/null @@ -1,39 +0,0 @@ -######### - Plugins -######### - -virtualenv can be extended via plugins using Python entry points. Plugins are automatically discovered from the Python -environment where virtualenv is installed, allowing you to customize how virtual environments are created, seeded, and -activated. - -****************** - Extension points -****************** - -virtualenv provides four extension points through entry point groups: - -``virtualenv.discovery`` - Python interpreter discovery plugins. These plugins locate and identify Python interpreters that will be used as the - base for creating virtual environments. - -``virtualenv.create`` - Virtual environment creator plugins. These plugins handle the actual creation of the virtual environment structure, - including copying or symlinking the Python interpreter and standard library. - -``virtualenv.seed`` - Seed package installer plugins. These plugins install initial packages (like pip, setuptools, wheel) into newly - created virtual environments. - -``virtualenv.activate`` - Shell activation script plugins. These plugins generate shell-specific activation scripts that modify the - environment to use the virtual environment. - -All extension points follow a common pattern: virtualenv discovers registered entry points, builds CLI options from -them, and executes the selected implementations during environment creation. - -.. toctree:: - - tutorial - how-to - api - architecture diff --git a/docs/plugin/tutorial.rst b/docs/plugin/tutorial.rst deleted file mode 100644 index abe20488d..000000000 --- a/docs/plugin/tutorial.rst +++ /dev/null @@ -1,116 +0,0 @@ -################### - Your first plugin -################### - -This tutorial walks through creating a simple discovery plugin that locates Python interpreters managed by pyenv. - -****************************** - Create the package structure -****************************** - -Set up a new Python package with the following structure: - -.. code-block:: text - - virtualenv-pyenv/ - โ”œโ”€โ”€ pyproject.toml - โ””โ”€โ”€ src/ - โ””โ”€โ”€ virtualenv_pyenv/ - โ””โ”€โ”€ __init__.py - -*************************** - Configure the entry point -*************************** - -In ``pyproject.toml``, declare your plugin as an entry point under the ``virtualenv.discovery`` group: - -.. code-block:: toml - - [project] - name = "virtualenv-pyenv" - version = "0.1.0" - dependencies = ["virtualenv>=20"] - - [project.entry-points."virtualenv.discovery"] - pyenv = "virtualenv_pyenv:PyEnvDiscovery" - - [build-system] - requires = ["setuptools>=61"] - build-backend = "setuptools.build_meta" - -********************** - Implement the plugin -********************** - -In ``src/virtualenv_pyenv/__init__.py``, implement the discovery plugin by subclassing ``Discover``: - -.. code-block:: python - - from __future__ import annotations - - import subprocess - from pathlib import Path - - from virtualenv.discovery.discover import Discover - from virtualenv.discovery.py_info import PythonInfo - - - class PyEnvDiscovery(Discover): - def __init__(self, options): - super().__init__(options) - self.python_spec = options.python if options.python else "python" - - @classmethod - def add_parser_arguments(cls, parser): - parser.add_argument( - "--python", - dest="python", - metavar="py", - type=str, - default=None, - help="pyenv Python version to use (e.g., 3.11.0)", - ) - - def run(self): - try: - result = subprocess.run( - ["pyenv", "which", "python"], - capture_output=True, - text=True, - check=True, - ) - python_path = Path(result.stdout.strip()) - return PythonInfo.from_exe(str(python_path)) - except (subprocess.CalledProcessError, FileNotFoundError) as e: - raise RuntimeError(f"Failed to locate pyenv Python: {e}") - -******************** - Install the plugin -******************** - -Install your plugin in development mode alongside virtualenv: - -.. code-block:: console - - $ pip install -e virtualenv-pyenv/ - -******************* - Verify the plugin -******************* - -Check that virtualenv recognizes your plugin by running: - -.. code-block:: console - - $ virtualenv --discovery help - -The output should list ``pyenv`` as an available discovery mechanism. You can now use it: - -.. code-block:: console - - $ virtualenv --discovery=pyenv myenv - created virtual environment CPython3.11.0.final.0-64 in 234ms - creator CPython3Posix(dest=/path/to/myenv, clear=False, no_vcs_ignore=False, global=False) - seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/path) - added seed packages: pip==23.0, setuptools==65.5.0, wheel==0.38.4 - activators BashActivator,CShellActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator diff --git a/docs/reference.rst b/docs/reference.rst new file mode 100644 index 000000000..9249473c9 --- /dev/null +++ b/docs/reference.rst @@ -0,0 +1,261 @@ +Reference Guide +=============== + +``virtualenv`` Command +---------------------- + +.. _usage: + +Usage +~~~~~ + +:command:`virtualenv [OPTIONS] ENV_DIR` + + Where ``ENV_DIR`` is an absolute or relative path to a directory to create + the virtual environment in. + +.. _options: + +Options +~~~~~~~ + +.. program: virtualenv + +.. option:: --version + + show program's version number and exit + +.. option:: -h, --help + + show this help message and exit + +.. option:: -v, --verbose + + Increase verbosity. + +.. option:: -q, --quiet + + Decrease verbosity. + +.. option:: -p PYTHON_EXE, --python=PYTHON_EXE + + The Python interpreter to use, e.g., + --python=python2.5 will use the python2.5 interpreter + to create the new environment. The default is the + interpreter that virtualenv was installed with + (like ``/usr/bin/python``) + +.. option:: --clear + + Clear out the non-root install and start from scratch. + +.. option:: --system-site-packages + + Give the virtual environment access to the global + site-packages. + +.. option:: --always-copy + + Always copy files rather than symlinking. + +.. option:: --relocatable + + Make an EXISTING virtualenv environment relocatable. + This fixes up scripts and makes all .pth files relative. + +.. option:: --unzip-setuptools + + Unzip Setuptools when installing it. + +.. option:: --no-setuptools + + Do not install setuptools in the new virtualenv. + +.. option:: --no-pip + + Do not install pip in the new virtualenv. + +.. option:: --no-wheel + + Do not install wheel in the new virtualenv. + +.. option:: --extra-search-dir=DIR + + Directory to look for setuptools/pip distributions in. + This option can be specified multiple times. + +.. option:: --prompt=PROMPT + + Provides an alternative prompt prefix for this + environment. + +.. option:: --download + + Download preinstalled packages from PyPI. + +.. option:: --no-download + + Do not download preinstalled packages from PyPI. + +.. option:: --no-site-packages + + DEPRECATED. Retained only for backward compatibility. + Not having access to global site-packages is now the + default behavior. + +.. option:: --distribute +.. option:: --setuptools + + Legacy; now have no effect. Before version 1.10 these could be used + to choose whether to install Distribute_ or Setuptools_ into the created + virtualenv. Distribute has now been merged into Setuptools, and the + latter is always installed. + +.. _Distribute: https://pypi.python.org/pypi/distribute +.. _Setuptools: https://pypi.python.org/pypi/setuptools + + +Configuration +------------- + +Environment Variables +~~~~~~~~~~~~~~~~~~~~~ + +Each command line option is automatically used to look for environment +variables with the name format ``VIRTUALENV_``. That means +the name of the command line options are capitalized and have dashes +(``'-'``) replaced with underscores (``'_'``). + +For example, to automatically use a custom Python binary instead of the +one virtualenv is run with you can also set an environment variable:: + + $ export VIRTUALENV_PYTHON=/opt/python-3.3/bin/python + $ virtualenv ENV + +It's the same as passing the option to virtualenv directly:: + + $ virtualenv --python=/opt/python-3.3/bin/python ENV + +This also works for appending command line options, like ``--find-links``. +Just leave an empty space between the passed values, e.g.:: + + $ export VIRTUALENV_EXTRA_SEARCH_DIR="/path/to/dists /path/to/other/dists" + $ virtualenv ENV + +is the same as calling:: + + $ virtualenv --extra-search-dir=/path/to/dists --extra-search-dir=/path/to/other/dists ENV + +.. envvar:: VIRTUAL_ENV_DISABLE_PROMPT + + Any virtualenv created when this is set to a non-empty value will not have + it's :ref:`activate` modify the shell prompt. + + +Configuration File +~~~~~~~~~~~~~~~~~~ + +virtualenv also looks for a standard ini config file. On Unix and Mac OS X +that's ``$HOME/.virtualenv/virtualenv.ini`` and on Windows, it's +``%APPDATA%\virtualenv\virtualenv.ini``. + +The names of the settings are derived from the long command line option, +e.g. the option :option:`--python <-p>` would look like this:: + + [virtualenv] + python = /opt/python-3.3/bin/python + +Appending options like :option:`--extra-search-dir` can be written on multiple +lines:: + + [virtualenv] + extra-search-dir = + /path/to/dists + /path/to/other/dists + +Please have a look at the output of :option:`--help <-h>` for a full list +of supported options. + + +Extending Virtualenv +-------------------- + + +Creating Your Own Bootstrap Scripts +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +While this creates an environment, it doesn't put anything into the +environment. Developers may find it useful to distribute a script +that sets up a particular environment, for example a script that +installs a particular web application. + +To create a script like this, call +:py:func:`virtualenv.create_bootstrap_script`, and write the +result to your new bootstrapping script. + +.. py:function:: create_bootstrap_script(extra_text) + + Creates a bootstrap script from ``extra_text``, which is like + this script but with extend_parser, adjust_options, and after_install hooks. + +This returns a string that (written to disk of course) can be used +as a bootstrap script with your own customizations. The script +will be the standard virtualenv.py script, with your extra text +added (your extra text should be Python code). + +If you include these functions, they will be called: + +.. py:function:: extend_parser(optparse_parser) + + You can add or remove options from the parser here. + +.. py:function:: adjust_options(options, args) + + You can change options here, or change the args (if you accept + different kinds of arguments, be sure you modify ``args`` so it is + only ``[DEST_DIR]``). + +.. py:function:: after_install(options, home_dir) + + After everything is installed, this function is called. This + is probably the function you are most likely to use. An + example would be:: + + def after_install(options, home_dir): + if sys.platform == 'win32': + bin = 'Scripts' + else: + bin = 'bin' + subprocess.call([join(home_dir, bin, 'easy_install'), + 'MyPackage']) + subprocess.call([join(home_dir, bin, 'my-package-script'), + 'setup', home_dir]) + + This example immediately installs a package, and runs a setup + script from that package. + +Bootstrap Example +~~~~~~~~~~~~~~~~~ + +Here's a more concrete example of how you could use this:: + + import virtualenv, textwrap + output = virtualenv.create_bootstrap_script(textwrap.dedent(""" + import os, subprocess + def after_install(options, home_dir): + etc = join(home_dir, 'etc') + if not os.path.exists(etc): + os.makedirs(etc) + subprocess.call([join(home_dir, 'bin', 'easy_install'), + 'BlogApplication']) + subprocess.call([join(home_dir, 'bin', 'paster'), + 'make-config', 'BlogApplication', + join(etc, 'blog.ini')]) + subprocess.call([join(home_dir, 'bin', 'paster'), + 'setup-app', join(etc, 'blog.ini')]) + """)) + f = open('blog-bootstrap.py', 'w').write(output) + +Another example is available `here`__. + +.. __: https://github.com/socialplanning/fassembler/blob/master/fassembler/create-venv-script.py diff --git a/docs/reference/api.rst b/docs/reference/api.rst deleted file mode 100644 index 30a5f6cf9..000000000 --- a/docs/reference/api.rst +++ /dev/null @@ -1,40 +0,0 @@ -.. _programmatic_api: - -######## - Python -######## - -The primary interface to ``virtualenv`` is the command line application. However, it can also be used programmatically -via the ``virtualenv.cli_run`` function and the ``Session`` class. - -See :doc:`../how-to/usage` for usage examples. - -******************* - virtualenv module -******************* - -.. automodule:: virtualenv - :members: - -*************** - Session class -*************** - -The ``Session`` class represents a virtualenv creation session and provides access to the created environment's -properties. - -.. currentmodule:: virtualenv.run.session - -.. autoclass:: Session - :members: - -******************* - VirtualEnvOptions -******************* - -Options namespace passed to plugin constructors, populated from the CLI, environment variables, and configuration files. - -.. currentmodule:: virtualenv.config.cli.parser - -.. autoclass:: VirtualEnvOptions - :members: diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst deleted file mode 100644 index bd7f889b5..000000000 --- a/docs/reference/cli.rst +++ /dev/null @@ -1,18 +0,0 @@ -############## - Command line -############## - -``virtualenv`` is primarily a command line application. All options have sensible defaults, and there is one required -argument: the name or path of the virtual environment to create. - -See :doc:`../how-to/usage` for how to select Python versions, configure defaults, and use environment variables. - -********************** - Command line options -********************** - -:command:`virtualenv [OPTIONS]` - -.. table_cli:: - :module: virtualenv.run - :func: build_parser_only diff --git a/docs/reference/compatibility.rst b/docs/reference/compatibility.rst deleted file mode 100644 index f7d60fe6d..000000000 --- a/docs/reference/compatibility.rst +++ /dev/null @@ -1,97 +0,0 @@ -.. _compatibility-requirements: - -############### - Compatibility -############### - -********************************** - Supported Python implementations -********************************** - -``virtualenv`` works with the following Python interpreter implementations. Only the latest patch version of each minor -version is fully supported; previous patch versions work on a best effort basis. - -CPython -======= - -``3.14 >= python_version >= 3.8`` - -PyPy -==== - -``3.11 >= python_version >= 3.8`` - -GraalPy -======= - -``24.1`` and later (Linux and macOS only). - -RustPython -========== - -Experimental support (Linux, macOS, and Windows). `RustPython `_ implements -Python 3.14. - -**************** - Support policy -**************** - -- **New versions** are added close to their release date, typically during the beta phase. -- **Old versions** are dropped 18 months after `CPython EOL `_, giving users - plenty of time to migrate. - -************************** - Version support timeline -************************** - -Major version support changes: - -- **20.27.0** (2024-10-17): dropped support for running under Python 3.7 and earlier. -- **20.22.0** (2023-04-19): dropped support for creating environments for Python 3.6 and earlier. -- **20.18.0** (2023-02-06): dropped support for running under Python 3.6 and earlier. - -***************************** - Supported operating systems -***************************** - -CPython is shipped in multiple forms, and each OS repackages it, often applying some customization. The platforms listed -below are tested. Unlisted platforms may work but are not explicitly supported. If you encounter issues on unlisted -platforms, please open a feature request. - -Cross-platform -============== - -These Python distributions work on Linux, macOS, and Windows: - -- Installations from `python.org `_ -- `python-build-standalone `_ builds (used by `uv - `_ and `mise `_) -- Python versions managed by `pyenv `_, `mise `_, or `asdf - `_ (shims are automatically resolved to the real binary) - -Linux -===== - -- Ubuntu 16.04 and later (both upstream and `deadsnakes `_ - builds) -- Fedora -- RHEL and CentOS -- OpenSuse -- Arch Linux - -macOS -===== - -- Python versions installed via `Homebrew `_ (works, but `not recommended - `_ -- Homebrew may upgrade or remove Python versions - without warning, breaking existing virtual environments) -- Python 3 part of XCode (Python framework builds at ``/Library/Frameworks/Python3.framework/``) - -.. note:: - - Framework builds do not support copy-based virtual environments. Use symlink or hardlink creation methods instead. - -Windows -======= - -- `Windows Store `_ Python 3.8 and later diff --git a/docs/render_cli.py b/docs/render_cli.py deleted file mode 100644 index ad77fc6a2..000000000 --- a/docs/render_cli.py +++ /dev/null @@ -1,239 +0,0 @@ -from __future__ import annotations - -from argparse import SUPPRESS -from contextlib import contextmanager -from typing import Any, ClassVar, NamedTuple - -from docutils import nodes as n -from docutils.parsers.rst.directives import unchanged_required -from sphinx.util.docutils import SphinxDirective -from sphinxarg.parser import parse_parser - -from virtualenv.run.plugin.base import ComponentBuilder - - -class TableRow(NamedTuple): - names: list[str] - default: str - choices: set[str] - help: str - - -class TextAsDefault(NamedTuple): - text: str - - -CUSTOM = { - "discovery": ComponentBuilder.entry_points_for("virtualenv.discovery"), - "creator": ComponentBuilder.entry_points_for("virtualenv.create"), - "seeder": ComponentBuilder.entry_points_for("virtualenv.seed"), - "activators": ComponentBuilder.entry_points_for("virtualenv.activate"), -} - - -class CliTable(SphinxDirective): - name: ClassVar[str] = "table_cli" - option_spec: ClassVar[str, Any] = {"module": unchanged_required, "func": unchanged_required} - - def run(self): - module_name, attr_name = self.options["module"], self.options["func"] - parser_creator = getattr(__import__(module_name, fromlist=[attr_name]), attr_name) - core_result = parse_parser(parser_creator()) - core_result["action_groups"] = [i for i in core_result["action_groups"] if i["title"] not in CUSTOM] - - content = [self._build_table(i["options"], i["title"], i["description"]) for i in core_result["action_groups"]] - for key, name_to_class in CUSTOM.items(): - section = n.section("", ids=[f"section-{key}"]) - title = n.title("", key) - section += title - self.state.document.note_implicit_target(title) - content.append(section) - results = {} - - for name, class_n in name_to_class.items(): - with self._run_parser(class_n, key, name): - cmd = [f"--{key}", name] - parser_result = parse_parser(parser_creator(cmd)) - opt_group = next(i["options"] for i in parser_result["action_groups"] if i["title"] == key) - results[name] = opt_group - core_names = set.intersection(*[{tuple(i["name"]) for i in v} for v in results.values()]) - if core_names: - rows = [i for i in next(iter(results.values())) if tuple(i["name"]) in core_names] - content.append( - self._build_table(rows, title="core", description=f"options shared across all {key}"), - ) - for name, group in results.items(): - rows = [i for i in group if tuple(i["name"]) not in core_names] - if rows: - content.append( - self._build_table(rows, title=name, description=f"options specific to {key} {name}"), - ) - return content - - @contextmanager - def _run_parser(self, class_n, key, name): - test_name = {"creator": "can_create", "activators": "supports"} - func_name = test_name.get(key) - try: - if func_name is not None: - prev = getattr(class_n, func_name) - - def a(*args, **kwargs): - prev(*args, **kwargs) - if key == "activators": - return True - if key == "creator": - if name == "venv": - from virtualenv.create.via_global_ref.venv import ViaGlobalRefMeta # noqa: PLC0415 - - meta = ViaGlobalRefMeta() - meta.symlink_error = None - return meta - from virtualenv.create.via_global_ref.builtin.via_global_self_do import ( # noqa: PLC0415 - BuiltinViaGlobalRefMeta, - ) - - meta = BuiltinViaGlobalRefMeta() - meta.symlink_error = None - return meta - raise RuntimeError - - setattr(class_n, func_name, a) - yield - finally: - if func_name is not None: - # noinspection PyUnboundLocalVariable - setattr(class_n, func_name, prev) - - def _build_table(self, options, title, description): - table = n.table() - table["classes"] += ["colwidths-auto"] - - options_group = n.tgroup(cols=3) - table += options_group - for _ in range(3): - options_group += n.colspec() - body = self._make_table_body(self.build_rows(options), title, description) - options_group += body - return table - - plugins: ClassVar[dict[str, str]] = { - "creator": "virtualenv.create", - "seed": "virtualenv.seed", - "activators": "virtualenv.activate", - "discovery": "virtualenv.discovery", - } - - @staticmethod - def build_rows(options): - result = [] - for option in options: - names = option["name"] - default = option["default"] - if ( - default is not None - and isinstance(default, str) - and default - and default[0] == default[-1] - and default[0] == '"' - ): - default = default[1:-1] - if default == SUPPRESS: - default = None - choices = option.get("choices") - key = names[0].strip("-") - if key in CliTable.plugins: - choices = list(ComponentBuilder.entry_points_for(CliTable.plugins[key]).keys()) - help_text = option["help"] - row = TableRow(names, default, choices, help_text) - result.append(row) - return result - - def _make_table_body(self, rows, title, description): - t_body = n.tbody() - header_row = n.paragraph() - header_row += n.strong(text=title) - if description: - header_row += n.Text(" โ‡’ ") - header_row += n.Text(description) - t_body += n.row("", n.entry("", header_row, morecols=2)) - for row in rows: - name_list = self._get_targeted_names(row) - default = CliTable._get_default(row) - help_text = CliTable._get_help_text(row) - row_node = n.row("", n.entry("", name_list), n.entry("", default), n.entry("", help_text)) - t_body += row_node - return t_body - - def _get_targeted_names(self, row): - names = [name.lstrip("-") for name in row.names] - target = n.target("", "", ids=names, names=names) - self.register_target_option(target) - first = True - for name, orig in zip(names, row.names): - if first: - first = False - else: - target += n.Text(", ") - self_ref = n.reference(refid=name) - self_ref += n.literal(text=orig) - target += self_ref - para = n.paragraph(text="") - para += target - return para - - @staticmethod - def _get_help_text(row): - name = row.names[0] - content = row.help[: row.help.index("(") - 1] if name == "--creator" else row.help - help_body = n.paragraph("", "", n.Text(content)) - if row.choices is not None: - help_body += n.Text("; choice of: ") - first = True - for choice in row.choices: - if first: - first = False - else: - help_body += n.Text(", ") - help_body += n.literal(text=choice) - return help_body - - @staticmethod - def _get_default(row): - default = row.default - name = row.names[0] - if name == "-p": - default_body = n.Text("the python executable virtualenv is installed into") - elif name == "--app-data": - default_body = n.Text("platform specific application data folder") - elif name == "--activators": - default_body = n.Text("comma separated list of activators supported") - elif name == "--creator": - default_body = n.paragraph("") - default_body += n.literal(text="builtin") - default_body += n.Text(" if exist, else ") - default_body += n.literal(text="venv") - elif default is None: - default_body = n.paragraph("", text="") - else: - default_body = n.literal(text=default if isinstance(default, str) else str(default)) - return default_body - - def register_target_option(self, target) -> None: - domain = self.env.get_domain("std") - self.state.document.note_explicit_target(target) - for key in target["ids"]: - domain.add_program_option(None, key, self.env.docname, key) - - -def literal_data(rawtext, app, of_type, slug, options): # noqa: ARG001 - """Create a link to a BitBucket resource.""" - of_class = of_type.split(".") - data = getattr(__import__(".".join(of_class[:-1]), fromlist=[of_class[-1]]), of_class[-1]) - return [n.literal("", text=",".join(data))], [] - - -__all__ = ( - "CliTable", - "literal_data", -) diff --git a/docs/tutorial/getting-started.rst b/docs/tutorial/getting-started.rst deleted file mode 100644 index 9a1611d10..000000000 --- a/docs/tutorial/getting-started.rst +++ /dev/null @@ -1,244 +0,0 @@ -################# - Getting started -################# - -This tutorial will teach you the basics of virtualenv through hands-on practice. You'll create your first virtual -environment, install packages, and learn how to manage project dependencies. - -*************** - Prerequisites -*************** - -Before starting this tutorial, you need: - -- Python 3.8 or later installed on your system. If you use a version manager like `pyenv - `_, `mise `_, or `asdf `_, virtualenv - will automatically discover the Python version they manage. -- virtualenv installed (see :doc:`../how-to/install`). - -*************************************** - Create your first virtual environment -*************************************** - -Let's create a virtual environment called ``myproject``: - -.. code-block:: console - - $ virtualenv myproject - created virtual environment CPython3.13.2.final.0-64 in 200ms - creator CPython3Posix(dest=/home/user/myproject, clear=False, no_vcs_ignore=False, global=False) - seeder FromAppData(download=False, pip=bundle, setuptools=bundle, via=copy, app_data_dir=/home/user/.cache/virtualenv) - activators BashActivator,CShellActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator - -This creates a new directory called ``myproject`` containing a complete, isolated Python environment with its own copy -of Python, pip, and other tools. - -************************** - Activate the environment -************************** - -To use your virtual environment, you can activate it. The activation command differs by platform: - -.. tab:: Linux/macOS - - .. code-block:: console - - $ source myproject/bin/activate - -.. tab:: Windows (PowerShell) - - .. code-block:: console - - PS> .\myproject\Scripts\Activate.ps1 - -.. tab:: Windows (CMD) - - .. code-block:: console - - C:\> .\myproject\Scripts\activate.bat - -After activation, your prompt changes to show the active environment: - -.. code-block:: console - - (myproject) $ - -You can verify that Python is now running from inside the virtual environment: - -.. tab:: Linux/macOS - - .. code-block:: console - - (myproject) $ which python - /home/user/myproject/bin/python - -.. tab:: Windows (PowerShell) - - .. code-block:: console - - (myproject) PS> where.exe python - C:\Users\user\myproject\Scripts\python.exe - -.. tab:: Windows (CMD) - - .. code-block:: console - - (myproject) C:\> where.exe python - C:\Users\user\myproject\Scripts\python.exe - -******************* - Install a package -******************* - -With the environment activated, install a package using pip: - -.. code-block:: console - - (myproject) $ pip install requests - Collecting requests - Using cached requests-2.32.3-py3-none-any.whl (64 kB) - Installing collected packages: requests - Successfully installed requests-2.32.3 - -Verify that the package is installed only inside your virtual environment: - -.. code-block:: console - - (myproject) $ python -c "import requests; print(requests.__file__)" - /home/user/myproject/lib/python3.13/site-packages/requests/__init__.py - -The path shows that ``requests`` is installed in the virtual environment, not in your system Python. - -************ - Deactivate -************ - -When you're done working in the virtual environment, deactivate it: - -.. code-block:: console - - (myproject) $ deactivate - $ - -The prompt returns to normal, and Python commands now use your system Python again. - -************************ - Use without activation -************************ - -Activation is a convenience, not a requirement. You can run any executable from the virtual environment directly by -using its full path: - -.. tab:: Linux/macOS - - .. code-block:: console - - $ myproject/bin/python -c "import sys; print(sys.prefix)" - /home/user/myproject - - $ myproject/bin/pip install httpx - -.. tab:: Windows (PowerShell) - - .. code-block:: console - - PS> .\myproject\Scripts\python.exe -c "import sys; print(sys.prefix)" - C:\Users\user\myproject - - PS> .\myproject\Scripts\pip.exe install httpx - -.. tab:: Windows (CMD) - - .. code-block:: console - - C:\> .\myproject\Scripts\python.exe -c "import sys; print(sys.prefix)" - C:\Users\user\myproject - - C:\> .\myproject\Scripts\pip.exe install httpx - -This is especially useful in scripts, CI pipelines, and automation where modifying the shell environment is unnecessary. - -*********************** - Set up a real project -*********************** - -Now let's apply what you've learned to a real project workflow: - -.. code-block:: console - - $ mkdir myapp && cd myapp - $ virtualenv venv - $ source venv/bin/activate # or use the appropriate command for your platform - (venv) $ pip install flask requests - (venv) $ pip freeze > requirements.txt - -The ``requirements.txt`` file now contains your project's dependencies: - -.. code-block:: text - - blinker==1.9.0 - certifi==2025.1.31 - charset-normalizer==3.4.1 - click==8.1.8 - flask==3.1.0 - idna==3.10 - itsdangerous==2.2.0 - Jinja2==3.1.5 - MarkupSafe==3.0.2 - requests==2.32.3 - urllib3==2.3.0 - werkzeug==3.1.3 - -This file lets you recreate the exact environment later. Let's test this: - -.. code-block:: console - - (venv) $ deactivate - $ rm -rf venv - $ virtualenv venv - $ source venv/bin/activate - (venv) $ pip install -r requirements.txt - -All packages are reinstalled exactly as before. Here's the complete workflow: - -.. mermaid:: - - graph TD - A[Create virtual environment] --> B[Activate] - B --> C[Install packages] - C --> D[Freeze to requirements.txt] - D --> E[Deactivate & clean up] - E --> F[Recreate virtual environment] - F --> G[Install from requirements.txt] - G --> H[Ready to work] - - style A fill:#2563eb,stroke:#1d4ed8,color:#fff - style B fill:#6366f1,stroke:#4f46e5,color:#fff - style C fill:#6366f1,stroke:#4f46e5,color:#fff - style D fill:#6366f1,stroke:#4f46e5,color:#fff - style E fill:#d97706,stroke:#b45309,color:#fff - style F fill:#6366f1,stroke:#4f46e5,color:#fff - style G fill:#6366f1,stroke:#4f46e5,color:#fff - style H fill:#16a34a,stroke:#15803d,color:#fff - -****************** - What you learned -****************** - -In this tutorial, you learned how to: - -- Create a virtual environment with ``virtualenv``. -- Activate and deactivate virtual environments on different platforms. -- Install packages in isolation from your system Python. -- Save project dependencies with ``pip freeze``. -- Reproduce environments using ``requirements.txt``. - -************ - Next steps -************ - -Now that you understand the basics, explore these topics: - -- :doc:`../how-to/usage` for selecting specific Python versions, configuring defaults, and advanced usage patterns. -- :doc:`../explanation` for understanding how virtualenv works under the hood and how it compares to ``venv``. -- :doc:`../reference/cli` for all available command line options and flags. diff --git a/docs/userguide.rst b/docs/userguide.rst new file mode 100644 index 000000000..c3eb031c3 --- /dev/null +++ b/docs/userguide.rst @@ -0,0 +1,258 @@ +User Guide +========== + + +Usage +----- + +Virtualenv has one basic command:: + + $ virtualenv ENV + +Where ``ENV`` is a directory to place the new virtual environment. It has +a number of usual effects (modifiable by many :ref:`options`): + + - :file:`ENV/lib/` and :file:`ENV/include/` are created, containing supporting + library files for a new virtualenv python. Packages installed in this + environment will live under :file:`ENV/lib/pythonX.X/site-packages/`. + + - :file:`ENV/bin` is created, where executables live - noticeably a new + :command:`python`. Thus running a script with ``#! /path/to/ENV/bin/python`` + would run that script under this virtualenv's python. + + - The crucial packages pip_ and setuptools_ are installed, which allow other + packages to be easily installed to the environment. This associated pip + can be run from :file:`ENV/bin/pip`. + +The python in your new virtualenv is effectively isolated from the python that +was used to create it. + +.. _pip: https://pypi.python.org/pypi/pip +.. _setuptools: https://pypi.python.org/pypi/setuptools + + +.. _activate: + +activate script +~~~~~~~~~~~~~~~ + +In a newly created virtualenv there will also be a :command:`activate` shell +script. For Windows systems, activation scripts are provided for +the Command Prompt and Powershell. + +On Posix systems, this resides in :file:`/ENV/bin/`, so you can run:: + + $ source bin/activate + +For some shells (e.g. the original Bourne Shell) you may need to use the +:command:`.` command, when :command:`source` does not exist. There are also +separate activate files for some other shells, like csh and fish. +:file:`bin/activate` should work for bash/zsh/dash. + +This will change your ``$PATH`` so its first entry is the virtualenv's +``bin/`` directory. (You have to use ``source`` because it changes your +shell environment in-place.) This is all it does; it's purely a +convenience. If you directly run a script or the python interpreter +from the virtualenv's ``bin/`` directory (e.g. ``path/to/ENV/bin/pip`` +or ``/path/to/ENV/bin/python-script.py``) there's no need for +activation. + +The ``activate`` script will also modify your shell prompt to indicate +which environment is currently active. To disable this behaviour, see +:envvar:`VIRTUAL_ENV_DISABLE_PROMPT`. + +To undo these changes to your path (and prompt), just run:: + + $ deactivate + +On Windows, the equivalent `activate` script is in the ``Scripts`` folder:: + + > \path\to\env\Scripts\activate + +And type ``deactivate`` to undo the changes. + +Based on your active shell (CMD.exe or Powershell.exe), Windows will use +either activate.bat or activate.ps1 (as appropriate) to activate the +virtual environment. If using Powershell, see the notes about code signing +below. + +.. note:: + + If using Powershell, the ``activate`` script is subject to the + `execution policies`_ on the system. By default on Windows 7, the system's + excution policy is set to ``Restricted``, meaning no scripts like the + ``activate`` script are allowed to be executed. But that can't stop us + from changing that slightly to allow it to be executed. + + In order to use the script, you can relax your system's execution + policy to ``AllSigned``, meaning all scripts on the system must be + digitally signed to be executed. Since the virtualenv activation + script is signed by one of the authors (Jannis Leidel) this level of + the execution policy suffices. As an administrator run:: + + PS C:\> Set-ExecutionPolicy AllSigned + + Then you'll be asked to trust the signer, when executing the script. + You will be prompted with the following:: + + PS C:\> virtualenv .\foo + New python executable in C:\foo\Scripts\python.exe + Installing setuptools................done. + Installing pip...................done. + PS C:\> .\foo\scripts\activate + + Do you want to run software from this untrusted publisher? + File C:\foo\scripts\activate.ps1 is published by E=jannis@leidel.info, + CN=Jannis Leidel, L=Berlin, S=Berlin, C=DE, Description=581796-Gh7xfJxkxQSIO4E0 + and is not trusted on your system. Only run scripts from trusted publishers. + [V] Never run [D] Do not run [R] Run once [A] Always run [?] Help + (default is "D"):A + (foo) PS C:\> + + If you select ``[A] Always Run``, the certificate will be added to the + Trusted Publishers of your user account, and will be trusted in this + user's context henceforth. If you select ``[R] Run Once``, the script will + be run, but you will be prompted on a subsequent invocation. Advanced users + can add the signer's certificate to the Trusted Publishers of the Computer + account to apply to all users (though this technique is out of scope of this + document). + + Alternatively, you may relax the system execution policy to allow running + of local scripts without verifying the code signature using the following:: + + PS C:\> Set-ExecutionPolicy RemoteSigned + + Since the ``activate.ps1`` script is generated locally for each virtualenv, + it is not considered a remote script and can then be executed. + +.. _`execution policies`: http://technet.microsoft.com/en-us/library/dd347641.aspx + +Removing an Environment +~~~~~~~~~~~~~~~~~~~~~~~ + +Removing a virtual environment is simply done by deactivating it and deleting the +environment folder with all its contents:: + + (ENV)$ deactivate + $ rm -r /path/to/ENV + +The :option:`--system-site-packages` Option +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you build with ``virtualenv --system-site-packages ENV``, your virtual +environment will inherit packages from ``/usr/lib/python2.7/site-packages`` +(or wherever your global site-packages directory is). + +This can be used if you have control over the global site-packages directory, +and you want to depend on the packages there. If you want isolation from the +global system, do not use this flag. + +Windows Notes +~~~~~~~~~~~~~ + +Some paths within the virtualenv are slightly different on Windows: scripts and +executables on Windows go in ``ENV\Scripts\`` instead of ``ENV/bin/`` and +libraries go in ``ENV\Lib\`` rather than ``ENV/lib/``. + +To create a virtualenv under a path with spaces in it on Windows, you'll need +the `win32api `_ library installed. + + +Using Virtualenv without ``bin/python`` +--------------------------------------- + +Sometimes you can't or don't want to use the Python interpreter +created by the virtualenv. For instance, in a `mod_python +`_ or `mod_wsgi `_ +environment, there is only one interpreter. + +Luckily, it's easy. You must use the custom Python interpreter to +*install* libraries. But to *use* libraries, you just have to be sure +the path is correct. A script is available to correct the path. You +can setup the environment like:: + + activate_this = '/path/to/env/bin/activate_this.py' + execfile(activate_this, dict(__file__=activate_this)) + +This will change ``sys.path`` and even change ``sys.prefix``, but also allow +you to use an existing interpreter. Items in your environment will show up +first on ``sys.path``, before global items. However, global items will +always be accessible (as if the :option:`--system-site-packages` flag had been +used in creating the environment, whether it was or not). Also, this cannot undo +the activation of other environments, or modules that have been imported. +You shouldn't try to, for instance, activate an environment before a web +request; you should activate *one* environment as early as possible, and not +do it again in that process. + +Making Environments Relocatable +------------------------------- + +**Note:** this option is somewhat experimental, and there are probably +caveats that have not yet been identified. + +.. warning:: + + The ``--relocatable`` option currently has a number of issues, + and is not guaranteed to work in all circumstances. It is possible + that the option will be deprecated in a future version of ``virtualenv``. + +Normally environments are tied to a specific path. That means that +you cannot move an environment around or copy it to another computer. +You can fix up an environment to make it relocatable with the +command:: + + $ virtualenv --relocatable ENV + +This will make some of the files created by setuptools use relative paths, +and will change all the scripts to use ``activate_this.py`` instead of using +the location of the Python interpreter to select the environment. + +**Note:** scripts which have been made relocatable will only work if +the virtualenv is activated, specifically the python executable from +the virtualenv must be the first one on the system PATH. Also note that +the activate scripts are not currently made relocatable by +``virtualenv --relocatable``. + +**Note:** you must run this after you've installed *any* packages into +the environment. If you make an environment relocatable, then +install a new package, you must run ``virtualenv --relocatable`` +again. + +Also, this **does not make your packages cross-platform**. You can +move the directory around, but it can only be used on other similar +computers. Some known environmental differences that can cause +incompatibilities: a different version of Python, when one platform +uses UCS2 for its internal unicode representation and another uses +UCS4 (a compile-time option), obvious platform changes like Windows +vs. Linux, or Intel vs. ARM, and if you have libraries that bind to C +libraries on the system, if those C libraries are located somewhere +different (either different versions, or a different filesystem +layout). + +If you use this flag to create an environment, currently, the +:option:`--system-site-packages` option will be implied. + +The :option:`--extra-search-dir` option +--------------------------------------- + +This option allows you to provide your own versions of setuptools and/or +pip to use instead of the embedded versions that come with virtualenv. + +To use this feature, pass one or more ``--extra-search-dir`` options to +virtualenv like this:: + + $ virtualenv --extra-search-dir=/path/to/distributions ENV + +The ``/path/to/distributions`` path should point to a directory that contains +setuptools and/or pip wheels. + +virtualenv will look for wheels in the specified directories, but will use +pip's standard algorithm for selecting the wheel to install, which looks for +the latest compatible wheel. + +As well as the extra directories, the search order includes: + +#. The ``virtualenv_support`` directory relative to virtualenv.py +#. The directory where virtualenv.py is located. +#. The current directory. + diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 60a26912e..000000000 --- a/pyproject.toml +++ /dev/null @@ -1,259 +0,0 @@ -[build-system] -build-backend = "hatchling.build" -requires = [ - "hatch-vcs>=0.4", - "hatchling>=1.27", -] - -[project] -name = "virtualenv" -description = "Virtual Python Environment builder" -readme = "README.md" -keywords = [ - "environments", - "isolated", - "virtual", -] -license = "MIT" -maintainers = [ - { name = "Bernat Gabor", email = "gaborjbernat@gmail.com" }, -] -requires-python = ">=3.8" -classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: MacOS :: MacOS X", - "Operating System :: Microsoft :: Windows", - "Operating System :: POSIX", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Topic :: Software Development :: Libraries", - "Topic :: Software Development :: Testing", - "Topic :: Utilities", -] -dynamic = [ - "version", -] -dependencies = [ - "distlib>=0.3.7,<1", - "filelock>=3.16.1,<=3.19.1; python_version<'3.10'", - "filelock>=3.24.2,<4; python_version>='3.10'", - "importlib-metadata>=6.6; python_version<'3.8'", - "platformdirs>=3.9.1,<5", - "python-discovery>=1", - "typing-extensions>=4.13.2; python_version<'3.11'", -] -urls.Documentation = "https://virtualenv.pypa.io" -urls.Homepage = "https://github.com/pypa/virtualenv" -urls.Source = "https://github.com/pypa/virtualenv" -urls.Tracker = "https://github.com/pypa/virtualenv/issues" -scripts.virtualenv = "virtualenv.__main__:run_with_catch" -entry-points."virtualenv.activate".bash = "virtualenv.activation.bash:BashActivator" -entry-points."virtualenv.activate".batch = "virtualenv.activation.batch:BatchActivator" -entry-points."virtualenv.activate".cshell = "virtualenv.activation.cshell:CShellActivator" -entry-points."virtualenv.activate".fish = "virtualenv.activation.fish:FishActivator" -entry-points."virtualenv.activate".nushell = "virtualenv.activation.nushell:NushellActivator" -entry-points."virtualenv.activate".powershell = "virtualenv.activation.powershell:PowerShellActivator" -entry-points."virtualenv.activate".python = "virtualenv.activation.python:PythonActivator" -entry-points."virtualenv.create".cpython3-mac-brew = "virtualenv.create.via_global_ref.builtin.cpython.mac_os:CPython3macOsBrew" -entry-points."virtualenv.create".cpython3-mac-framework = "virtualenv.create.via_global_ref.builtin.cpython.mac_os:CPython3macOsFramework" -entry-points."virtualenv.create".cpython3-posix = "virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Posix" -entry-points."virtualenv.create".cpython3-win = "virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Windows" -entry-points."virtualenv.create".graalpy-posix = "virtualenv.create.via_global_ref.builtin.graalpy:GraalPyPosix" -entry-points."virtualenv.create".graalpy-win = "virtualenv.create.via_global_ref.builtin.graalpy:GraalPyWindows" -entry-points."virtualenv.create".pypy3-posix = "virtualenv.create.via_global_ref.builtin.pypy.pypy3:PyPy3Posix" -entry-points."virtualenv.create".pypy3-win = "virtualenv.create.via_global_ref.builtin.pypy.pypy3:Pypy3Windows" -entry-points."virtualenv.create".rustpython-posix = "virtualenv.create.via_global_ref.builtin.rustpython:RustPythonPosix" -entry-points."virtualenv.create".rustpython-win = "virtualenv.create.via_global_ref.builtin.rustpython:RustPythonWindows" -entry-points."virtualenv.create".venv = "virtualenv.create.via_global_ref.venv:Venv" -entry-points."virtualenv.discovery".builtin = "virtualenv.discovery.builtin:Builtin" -entry-points."virtualenv.seed".app-data = "virtualenv.seed.embed.via_app_data.via_app_data:FromAppData" -entry-points."virtualenv.seed".pip = "virtualenv.seed.embed.pip_invoke:PipInvoke" - -[dependency-groups] -dev = [ - { include-group = "docs" }, - { include-group = "lint" }, - { include-group = "pkg-meta" }, - { include-group = "test" }, - { include-group = "type" }, -] -test = [ - "covdefaults>=2.3", - "coverage>=7.2.7", - "coverage-enable-subprocess>=1", - "flaky>=3.7", - "packaging>=23.1", - "pytest>=7.4", - "pytest-env>=0.8.2", - """\ - pytest-freezer>=0.4.8; platform_python_implementation=='PyPy' or platform_python_implementation=='GraalVM' or \ - platform_python_implementation=='RustPython' or (platform_python_implementation=='CPython' and sys_platform=='win32' \ - and python_version>='3.13')\ - """, - "pytest-mock>=3.11.1", - "pytest-randomly>=3.12", - "pytest-timeout>=2.1", - "pytest-xdist>=3.5", - "setuptools>=68", - "time-machine>=2.10; platform_python_implementation=='CPython'", -] -type = [ - "ty>=0.0.17", - { include-group = "test" }, -] -docs = [ - "furo>=2023.7.26", - "pre-commit-uv>=4.2", - "proselint>=0.13", - "sphinx>=7.1.2,!=7.3", - "sphinx-argparse>=0.4", - "sphinx-autodoc-typehints>=3.6.2", - "sphinx-copybutton>=0.5.2", - "sphinx-inline-tabs>=2025.12.21.14", - "sphinxcontrib-mermaid>=2", - "sphinxcontrib-towncrier>=0.2.1a0", - "towncrier>=23.6", -] -lint = [ - "pre-commit-uv>=4.2", -] -pkg-meta = [ - "check-wheel-contents>=0.6.3", - "twine>=6.2", - "uv>=0.10.2", -] - -[tool.hatch] -build.hooks.vcs.version-file = "src/virtualenv/version.py" -build.targets.sdist.include = [ - "/src", - "/tests", - "/tasks", - "/tox.toml", -] -version.source = "vcs" - -[tool.ruff] -line-length = 120 -format.preview = true -format.docstring-code-line-length = 100 -format.docstring-code-format = true -lint.select = [ - "ALL", -] -lint.ignore = [ - "COM812", # conflict with formatter - "CPY", # No copyright header - "D10", # no docstrings - "D40", # no imperative mode for docstrings - "D200", # fits on one line - conflicts with docstrfmt - "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible - "D205", # 1 blank line required between summary and description - conflicts with docstrfmt - "D209", # multi-line docstring closing quotes - conflicts with docstrfmt - "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible - "D213", # multi-line-summary-second-line - conflicts with docstrfmt - "D301", # use r if any backslashes - conflicts with docstrfmt - "DOC", # no restructuredtext support - "E501", # line too long - handled by ruff format and docstrfmt - "FBT001", # boolean positional args - pre-existing signatures, changing would break public API - "INP001", # ignore implicit namespace packages - "ISC001", # conflict with formatter - "PLR0914", # Too many local variables - "PLR0917", # Too many positional arguments - "PLR6301", # Method could be a function, class method, or static method - "PLW1510", # no need for check for subprocess - "PTH", # no pathlib, <=39 has problems on Windows with absolute/resolve, can revisit once we no longer need 39 - "RUF067", # `__init__` module should only contain docstrings and re-exports - "S104", # Possible binding to all interfaces - "S404", # Using subprocess is alright - "S603", # subprocess calls are fine -] -lint.per-file-ignores."docs/**/*.py" = [ - "ANN", # don't require type annotations in docs scripts -] -lint.per-file-ignores."src/virtualenv/activation/python/activate_this.py" = [ - "F821", # ignore undefined template string placeholders -] -lint.per-file-ignores."tests/**/*.py" = [ - "ANN", # don't require type annotations in tests (pytest fixtures make this impractical) - "D", # don't care about documentation in tests - "FBT", # don't care about booleans as positional arguments in tests - "INP001", # no implicit namespace - "PLC2701", # Private import - "PLR2004", # Magic value used in comparison, consider replacing with a constant variable - "S101", # asserts allowed in tests - "S603", # `subprocess` call: check for execution of untrusted input - "S607", # partial executable path is fine in tests -] -lint.isort = { known-first-party = [ - "virtualenv", -], required-imports = [ - "from __future__ import annotations", -] } -lint.preview = true - -[tool.codespell] -builtin = "clear,usage,en-GB_to_en-US" -count = true - -[tool.pyproject-fmt] -max_supported_python = "3.14" - -[tool.ty] -rules.unused-ignore-comment = "ignore" # some ignores are platform/version-specific (e.g., ctypes.windll on Linux) - -[tool.pytest] -ini_options.markers = [ - "slow", - "graalpy: minimal test suite for GraalPy validation", -] -ini_options.timeout = 120 -ini_options.addopts = "--showlocals --no-success-flaky-report" -ini_options.env = [ - "PYTHONIOENCODING=utf-8", -] - -[tool.coverage] -run.dynamic_context = "test_function" -run.parallel = true -run.plugins = [ - "covdefaults", -] -run.relative_files = true -run.source = [ - "${_COVERAGE_SRC}", - "tests", -] -paths.source = [ - "src", - "**/site-packages", -] -report.fail_under = 76 -report.omit = [ - "**/src/virtualenv/activation/python/activate_this.py", - "**/src/virtualenv/create/via_global_ref/_virtualenv.py", - # site.py is run before the coverage can be enabled, no way to measure coverage on this - "**/src/virtualenv/create/via_global_ref/builtin/python2/site.py", - "**/src/virtualenv/seed/wheels/embed/pip-*.whl/pip/**", -] -html.show_contexts = true -html.skip_covered = false - -[tool.towncrier] -name = "virtualenv" -filename = "docs/changelog.rst" -directory = "docs/changelog" -title_format = false -issue_format = ":issue:`{issue}`" -template = "docs/changelog/template.jinja2" -underlines = [ "*", "=", "-" ] diff --git a/scripts/virtualenv b/scripts/virtualenv new file mode 100644 index 000000000..c961dd7db --- /dev/null +++ b/scripts/virtualenv @@ -0,0 +1,3 @@ +#!/usr/bin/env python +import virtualenv +virtualenv.main() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..3c6e79cf3 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..ee03bc531 --- /dev/null +++ b/setup.py @@ -0,0 +1,123 @@ +import os +import re +import shutil +import sys + +if sys.version_info[:2] < (2, 6): + sys.exit('virtualenv requires Python 2.6 or higher.') + +try: + from setuptools import setup + from setuptools.command.test import test as TestCommand + + class PyTest(TestCommand): + user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")] + + def initialize_options(self): + TestCommand.initialize_options(self) + self.pytest_args = [] + + def finalize_options(self): + TestCommand.finalize_options(self) + #self.test_args = [] + #self.test_suite = True + + def run_tests(self): + # import here, because outside the eggs aren't loaded + import pytest + sys.exit(pytest.main(self.pytest_args)) + + setup_params = { + 'entry_points': { + 'console_scripts': ['virtualenv=virtualenv:main'], + }, + 'zip_safe': False, + 'cmdclass': {'test': PyTest}, + 'tests_require': ['pytest', 'mock'], + } +except ImportError: + from distutils.core import setup + if sys.platform == 'win32': + print('Note: without Setuptools installed you will ' + 'have to use "python -m virtualenv ENV"') + setup_params = {} + else: + script = 'scripts/virtualenv' + setup_params = {'scripts': [script]} + + +def read_file(*paths): + here = os.path.dirname(os.path.abspath(__file__)) + with open(os.path.join(here, *paths)) as f: + return f.read() + +# Get long_description from index.rst: +long_description = read_file('docs', 'index.rst') +long_description = long_description.strip().split('split here', 1)[0] +# Add release history +changes = read_file('docs', 'changes.rst') +# Only report last two releases for brevity +releases_found = 0 +change_lines = [] +for line in changes.splitlines(): + change_lines.append(line) + if line.startswith('--------------'): + releases_found += 1 + if releases_found > 2: + break + +changes = '\n'.join(change_lines[:-2]) + '\n' +changes += '`Full Changelog `_.' +# Replace issue/pull directives +changes = re.sub(r':pull:`(\d+)`', r'PR #\1', changes) +changes = re.sub(r':issue:`(\d+)`', r'#\1', changes) + +long_description += '\n\n' + changes + + +def get_version(): + version_file = read_file('virtualenv.py') + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", + version_file, re.M) + if version_match: + return version_match.group(1) + raise RuntimeError("Unable to find version string.") + + +# Hack to prevent stupid TypeError: 'NoneType' object is not callable error on +# exit of python setup.py test # in multiprocessing/util.py _exit_function when +# running python setup.py test (see +# http://www.eby-sarna.com/pipermail/peak/2010-May/003357.html) +try: + import multiprocessing # noqa +except ImportError: + pass + +setup( + name='virtualenv', + version=get_version(), + description="Virtual Python Environment builder", + long_description=long_description, + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + ], + keywords='setuptools deployment installation distutils', + author='Ian Bicking', + author_email='ianb@colorstudy.com', + maintainer='Jannis Leidel, Carl Meyer and Brian Rosner', + maintainer_email='python-virtualenv@groups.google.com', + url='https://virtualenv.pypa.io/', + license='MIT', + py_modules=['virtualenv'], + packages=['virtualenv_support'], + package_data={'virtualenv_support': ['*.whl']}, + **setup_params) diff --git a/src/virtualenv/__init__.py b/src/virtualenv/__init__.py deleted file mode 100644 index cc11e7f3e..000000000 --- a/src/virtualenv/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from __future__ import annotations - -from .run import cli_run, session_via_cli -from .version import __version__ - -__all__ = [ - "__version__", - "cli_run", - "session_via_cli", -] diff --git a/src/virtualenv/__main__.py b/src/virtualenv/__main__.py deleted file mode 100644 index 1cfffe266..000000000 --- a/src/virtualenv/__main__.py +++ /dev/null @@ -1,92 +0,0 @@ -from __future__ import annotations - -import errno -import logging -import os -import sys -from timeit import default_timer -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import MutableMapping - - from virtualenv.config.cli.parser import VirtualEnvOptions - from virtualenv.run.session import Session - -LOGGER = logging.getLogger(__name__) - - -def run( - args: list[str] | None = None, options: VirtualEnvOptions | None = None, env: MutableMapping[str, str] | None = None -) -> None: - env = os.environ if env is None else env - start = default_timer() - from virtualenv.run import cli_run # noqa: PLC0415 - from virtualenv.util.error import ProcessCallFailedError # noqa: PLC0415 - - if args is None: - args = sys.argv[1:] - try: - session = cli_run(args, options, env=env) - LOGGER.warning(LogSession(session, start)) - except ProcessCallFailedError as exception: - print(f"subprocess call failed for {exception.cmd} with code {exception.code}") # noqa: T201 - print(exception.out, file=sys.stdout, end="") # noqa: T201 - print(exception.err, file=sys.stderr, end="") # noqa: T201 - raise SystemExit(exception.code) # noqa: B904 - except OSError as exception: - if exception.errno == errno.EMFILE: - print( # noqa: T201 - "OSError: [Errno 24] Too many open files. You may need to increase your OS open files limit.\n" - " On macOS/Linux, try 'ulimit -n 2048'.\n" - " For Windows, this is not a common issue, but you can try to close some applications.", - file=sys.stderr, - ) - raise - - -class LogSession: - def __init__(self, session: Session, start: float) -> None: - self.session = session - self.start = start - - def __str__(self) -> str: - spec = self.session.creator.interpreter.spec - elapsed = (default_timer() - self.start) * 1000 - lines = [ - f"created virtual environment {spec} in {elapsed:.0f}ms", - f" creator {self.session.creator!s}", - ] - if self.session.seeder.enabled: - lines.append(f" seeder {self.session.seeder!s}") - path = self.session.creator.purelib.iterdir() - packages = sorted("==".join(i.stem.split("-")) for i in path if i.suffix == ".dist-info") - lines.append(f" added seed packages: {', '.join(packages)}") - - if self.session.activators: - lines.append(f" activators {','.join(i.__class__.__name__ for i in self.session.activators)}") - return "\n".join(lines) - - -def run_with_catch(args: list[str] | None = None, env: MutableMapping[str, str] | None = None) -> None: - from virtualenv.config.cli.parser import VirtualEnvOptions # noqa: PLC0415 - - env = os.environ if env is None else env - options = VirtualEnvOptions() - try: - run(args, options, env) - except (KeyboardInterrupt, SystemExit, Exception) as exception: # noqa: BLE001 - try: - if getattr(options, "with_traceback", False): - raise - if not (isinstance(exception, SystemExit) and exception.code == 0): - LOGGER.error("%s: %s", type(exception).__name__, exception) # noqa: TRY400 - code = exception.code if isinstance(exception, SystemExit) else 1 - sys.exit(code) - finally: - for handler in LOGGER.handlers: # force flush of log messages before the trace is printed - handler.flush() - - -if __name__ == "__main__": # pragma: no cov - run_with_catch() # pragma: no cov diff --git a/src/virtualenv/activation/__init__.py b/src/virtualenv/activation/__init__.py deleted file mode 100644 index 5aa4a9d76..000000000 --- a/src/virtualenv/activation/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -from __future__ import annotations - -from .bash import BashActivator -from .batch import BatchActivator -from .cshell import CShellActivator -from .fish import FishActivator -from .nushell import NushellActivator -from .powershell import PowerShellActivator -from .python import PythonActivator - -__all__ = [ - "BashActivator", - "BatchActivator", - "CShellActivator", - "FishActivator", - "NushellActivator", - "PowerShellActivator", - "PythonActivator", -] diff --git a/src/virtualenv/activation/activator.py b/src/virtualenv/activation/activator.py deleted file mode 100644 index 159a3e99f..000000000 --- a/src/virtualenv/activation/activator.py +++ /dev/null @@ -1,61 +0,0 @@ -from __future__ import annotations - -import os -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from argparse import ArgumentParser - from pathlib import Path - - from python_discovery import PythonInfo - - from virtualenv.config.cli.parser import VirtualEnvOptions - from virtualenv.create.creator import Creator - - -class Activator(ABC): - """Generates activate script for the virtual environment.""" - - def __init__(self, options: VirtualEnvOptions) -> None: - """Create a new activator generator. - - :param options: the parsed options as defined within :meth:`add_parser_arguments` - - """ - self.flag_prompt = os.path.basename(os.getcwd()) if options.prompt == "." else options.prompt - - @classmethod - def supports(cls, interpreter: PythonInfo) -> bool: # noqa: ARG003 - """Check if the activation script is supported in the given interpreter. - - :param interpreter: the interpreter we need to support - - :returns: ``True`` if supported, ``False`` otherwise - - """ - return True - - @classmethod # noqa: B027 - def add_parser_arguments(cls, parser: ArgumentParser, interpreter: PythonInfo) -> None: - """Add CLI arguments for this activation script. - - :param parser: the CLI parser - :param interpreter: the interpreter this virtual environment is based of - - """ - - @abstractmethod - def generate(self, creator: Creator) -> list[Path]: - """Generate activate script for the given creator. - - :param creator: the creator (based of :class:`virtualenv.create.creator.Creator`) we used to create this virtual - environment - - """ - raise NotImplementedError - - -__all__ = [ - "Activator", -] diff --git a/src/virtualenv/activation/bash/__init__.py b/src/virtualenv/activation/bash/__init__.py deleted file mode 100644 index e083e9585..000000000 --- a/src/virtualenv/activation/bash/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -from typing import TYPE_CHECKING - -from virtualenv.activation.via_template import ViaTemplateActivator - -if TYPE_CHECKING: - from collections.abc import Iterator - - from virtualenv.create.creator import Creator - - -class BashActivator(ViaTemplateActivator): - def templates(self) -> Iterator[str]: - yield "activate.sh" - - def as_name(self, template: str) -> str: - return Path(template).stem - - def replacements(self, creator: Creator, dest_folder: Path) -> dict[str, str]: - data = super().replacements(creator, dest_folder) - data.update({ - "__TCL_LIBRARY__": getattr(creator.interpreter, "tcl_lib", None) or "", - "__TK_LIBRARY__": getattr(creator.interpreter, "tk_lib", None) or "", - }) - return data - - -__all__ = [ - "BashActivator", -] diff --git a/src/virtualenv/activation/bash/activate.sh b/src/virtualenv/activation/bash/activate.sh deleted file mode 100644 index b418101d4..000000000 --- a/src/virtualenv/activation/bash/activate.sh +++ /dev/null @@ -1,132 +0,0 @@ -# This file must be used with "source bin/activate" *from bash* -# you cannot run it directly - - -if [ "${BASH_SOURCE-}" = "$0" ]; then - echo "You must source this script: \$ source $0" >&2 - exit 33 -fi - -deactivate () { - unset -f pydoc >/dev/null 2>&1 || true - - # reset old environment variables - if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then - PATH="$_OLD_VIRTUAL_PATH" - export PATH - unset _OLD_VIRTUAL_PATH - fi - if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then - PYTHONHOME="$_OLD_VIRTUAL_PYTHONHOME" - export PYTHONHOME - unset _OLD_VIRTUAL_PYTHONHOME - fi - - if [ -n "${_OLD_VIRTUAL_TCL_LIBRARY:-}" ]; then - TCL_LIBRARY="$_OLD_VIRTUAL_TCL_LIBRARY" - export TCL_LIBRARY - unset _OLD_VIRTUAL_TCL_LIBRARY - fi - if [ -n "${_OLD_VIRTUAL_TK_LIBRARY:-}" ]; then - TK_LIBRARY="$_OLD_VIRTUAL_TK_LIBRARY" - export TK_LIBRARY - unset _OLD_VIRTUAL_TK_LIBRARY - fi - if [ -n "${_OLD_PKG_CONFIG_PATH:-}" ]; then - PKG_CONFIG_PATH="$_OLD_PKG_CONFIG_PATH" - export PKG_CONFIG_PATH - unset _OLD_PKG_CONFIG_PATH - fi - - # The hash command must be called to get it to forget past - # commands. Without forgetting past commands the $PATH changes - # we made may not be respected - hash -r 2>/dev/null - - if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then - PS1="$_OLD_VIRTUAL_PS1" - export PS1 - unset _OLD_VIRTUAL_PS1 - fi - - unset VIRTUAL_ENV - unset VIRTUAL_ENV_PROMPT - if [ ! "${1-}" = "nondestructive" ] ; then - # Self destruct! - unset -f deactivate - fi -} - -# unset irrelevant variables -deactivate nondestructive - -if [ ! -d __VIRTUAL_ENV__ ]; then - echo "Virtual environment directory __VIRTUAL_ENV__ does not exist!" >&2 - CURRENT_PATH=$(realpath "${BASH_SOURCE[0]}") - CURRENT_DIR=$(dirname "${CURRENT_PATH}") - VIRTUAL_ENV="$(realpath "${CURRENT_DIR}/../")" -else - VIRTUAL_ENV=__VIRTUAL_ENV__ -fi - -case "$(uname)" in - CYGWIN*|MSYS*|MINGW*) - VIRTUAL_ENV=$(cygpath "$VIRTUAL_ENV") - ;; -esac -export VIRTUAL_ENV - -_OLD_VIRTUAL_PATH="$PATH" -PATH="$VIRTUAL_ENV/"__BIN_NAME__":$PATH" -export PATH - -_OLD_PKG_CONFIG_PATH="${PKG_CONFIG_PATH:-}" -PKG_CONFIG_PATH="${VIRTUAL_ENV}/lib/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}" -export PKG_CONFIG_PATH - -if [ "x"__VIRTUAL_PROMPT__ != x ] ; then - VIRTUAL_ENV_PROMPT=__VIRTUAL_PROMPT__ -else - VIRTUAL_ENV_PROMPT=$(basename "$VIRTUAL_ENV") -fi -export VIRTUAL_ENV_PROMPT - -# unset PYTHONHOME if set -if [ -n "${PYTHONHOME:-}" ] ; then - _OLD_VIRTUAL_PYTHONHOME="$PYTHONHOME" - unset PYTHONHOME -fi - -if [ __TCL_LIBRARY__ != "" ]; then - if [ -n "${TCL_LIBRARY:-}" ] ; then - _OLD_VIRTUAL_TCL_LIBRARY="$TCL_LIBRARY" - fi - TCL_LIBRARY=__TCL_LIBRARY__ - export TCL_LIBRARY -fi - -if [ __TK_LIBRARY__ != "" ]; then - if [ -n "${TK_LIBRARY:-}" ] ; then - _OLD_VIRTUAL_TK_LIBRARY="$TK_LIBRARY" - fi - TK_LIBRARY=__TK_LIBRARY__ - export TK_LIBRARY -fi - -if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then - _OLD_VIRTUAL_PS1="${PS1-}" - PS1="(${VIRTUAL_ENV_PROMPT}) ${PS1-}" - export PS1 -fi - -# Make sure to unalias pydoc if it's already there -alias pydoc 2>/dev/null >/dev/null && unalias pydoc || true - -pydoc () { - python -m pydoc "$@" -} - -# The hash command must be called to get it to forget past -# commands. Without forgetting past commands the $PATH changes -# we made may not be respected -hash -r 2>/dev/null || true diff --git a/src/virtualenv/activation/batch/__init__.py b/src/virtualenv/activation/batch/__init__.py deleted file mode 100644 index f54a1d3c8..000000000 --- a/src/virtualenv/activation/batch/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -from __future__ import annotations - -import os -from typing import TYPE_CHECKING - -from virtualenv.activation.via_template import ViaTemplateActivator - -if TYPE_CHECKING: - from collections.abc import Iterator - - from python_discovery import PythonInfo - - from virtualenv.create.creator import Creator - - -class BatchActivator(ViaTemplateActivator): - @classmethod - def supports(cls, interpreter: PythonInfo) -> bool: - return interpreter.os == "nt" - - def templates(self) -> Iterator[str]: - yield "activate.bat" - yield "deactivate.bat" - yield "pydoc.bat" - - @staticmethod - def quote(string: str) -> str: - return string - - def instantiate_template(self, replacements: dict[str, str], template: str, creator: Creator) -> str: - # ensure the text has all newlines as \r\n - required by batch - base = super().instantiate_template(replacements, template, creator) - return base.replace(os.linesep, "\n").replace("\n", os.linesep) - - -__all__ = [ - "BatchActivator", -] diff --git a/src/virtualenv/activation/batch/activate.bat b/src/virtualenv/activation/batch/activate.bat deleted file mode 100644 index 06192921f..000000000 --- a/src/virtualenv/activation/batch/activate.bat +++ /dev/null @@ -1,59 +0,0 @@ -@REM This file is UTF-8 encoded, so we need to update the current code page while executing it -@for /f "tokens=2 delims=:." %%a in ('"%SystemRoot%\System32\chcp.com"') do @set _OLD_CODEPAGE=%%a - -@if defined _OLD_CODEPAGE ( - "%SystemRoot%\System32\chcp.com" 65001 > nul -) - -@set "VIRTUAL_ENV=__VIRTUAL_ENV__" - -@set "VIRTUAL_ENV_PROMPT=__VIRTUAL_PROMPT__" -@if NOT DEFINED VIRTUAL_ENV_PROMPT ( - @for %%d in ("%VIRTUAL_ENV%") do @set "VIRTUAL_ENV_PROMPT=%%~nxd" -) - -@if defined _OLD_VIRTUAL_PROMPT ( - @set "PROMPT=%_OLD_VIRTUAL_PROMPT%" -) else ( - @if not defined PROMPT ( - @set "PROMPT=$P$G" - ) - @if not defined VIRTUAL_ENV_DISABLE_PROMPT ( - @set "_OLD_VIRTUAL_PROMPT=%PROMPT%" - ) -) -@if not defined VIRTUAL_ENV_DISABLE_PROMPT ( - @set "PROMPT=(%VIRTUAL_ENV_PROMPT%) %PROMPT%" -) - -@REM Don't use () to avoid problems with them in %PATH% -@if defined _OLD_VIRTUAL_PYTHONHOME @goto ENDIFVHOME - @set "_OLD_VIRTUAL_PYTHONHOME=%PYTHONHOME%" -:ENDIFVHOME - -@set PYTHONHOME= - -@if defined TCL_LIBRARY @set "_OLD_VIRTUAL_TCL_LIBRARY=%TCL_LIBRARY%" -@if NOT "__TCL_LIBRARY__"=="" @set "TCL_LIBRARY=__TCL_LIBRARY__" - -@if defined TK_LIBRARY @set "_OLD_VIRTUAL_TK_LIBRARY=%TK_LIBRARY%" -@if NOT "__TK_LIBRARY__"=="" @set "TK_LIBRARY=__TK_LIBRARY__" - -@if defined PKG_CONFIG_PATH @set "_OLD_PKG_CONFIG_PATH=%PKG_CONFIG_PATH%" -@set "PKG_CONFIG_PATH=%VIRTUAL_ENV%\lib\pkgconfig;%PKG_CONFIG_PATH%" - -@REM if defined _OLD_VIRTUAL_PATH ( -@if not defined _OLD_VIRTUAL_PATH @goto ENDIFVPATH1 - @set "PATH=%_OLD_VIRTUAL_PATH%" -:ENDIFVPATH1 -@REM ) else ( -@if defined _OLD_VIRTUAL_PATH @goto ENDIFVPATH2 - @set "_OLD_VIRTUAL_PATH=%PATH%" -:ENDIFVPATH2 - -@set "PATH=%VIRTUAL_ENV%\__BIN_NAME__;%PATH%" - -@if defined _OLD_CODEPAGE ( - "%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul - @set _OLD_CODEPAGE= -) diff --git a/src/virtualenv/activation/batch/deactivate.bat b/src/virtualenv/activation/batch/deactivate.bat deleted file mode 100644 index d486958ad..000000000 --- a/src/virtualenv/activation/batch/deactivate.bat +++ /dev/null @@ -1,30 +0,0 @@ -@set VIRTUAL_ENV= -@set VIRTUAL_ENV_PROMPT= - -@REM Don't use () to avoid problems with them in %PATH% -@if not defined _OLD_VIRTUAL_PROMPT @goto ENDIFVPROMPT - @set "PROMPT=%_OLD_VIRTUAL_PROMPT%" - @set _OLD_VIRTUAL_PROMPT= -:ENDIFVPROMPT - -@if not defined _OLD_VIRTUAL_PYTHONHOME @goto ENDIFVHOME - @set "PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%" - @set _OLD_VIRTUAL_PYTHONHOME= -:ENDIFVHOME - -@if defined _OLD_VIRTUAL_TCL_LIBRARY @set "TCL_LIBRARY=%_OLD_VIRTUAL_TCL_LIBRARY%" -@if not defined _OLD_VIRTUAL_TCL_LIBRARY @set TCL_LIBRARY= -@set _OLD_VIRTUAL_TCL_LIBRARY= - -@if defined _OLD_VIRTUAL_TK_LIBRARY @set "TK_LIBRARY=%_OLD_VIRTUAL_TK_LIBRARY%" -@if not defined _OLD_VIRTUAL_TK_LIBRARY @set TK_LIBRARY= -@set _OLD_VIRTUAL_TK_LIBRARY= - -@if defined _OLD_PKG_CONFIG_PATH @set "PKG_CONFIG_PATH=%_OLD_PKG_CONFIG_PATH%" -@if not defined _OLD_PKG_CONFIG_PATH @set PKG_CONFIG_PATH= -@set _OLD_PKG_CONFIG_PATH= - -@if not defined _OLD_VIRTUAL_PATH @goto ENDIFVPATH - @set "PATH=%_OLD_VIRTUAL_PATH%" - @set _OLD_VIRTUAL_PATH= -:ENDIFVPATH diff --git a/src/virtualenv/activation/batch/pydoc.bat b/src/virtualenv/activation/batch/pydoc.bat deleted file mode 100644 index d55f8ac03..000000000 --- a/src/virtualenv/activation/batch/pydoc.bat +++ /dev/null @@ -1 +0,0 @@ -"python.exe" -m pydoc %* diff --git a/src/virtualenv/activation/cshell/__init__.py b/src/virtualenv/activation/cshell/__init__.py deleted file mode 100644 index 11f48a671..000000000 --- a/src/virtualenv/activation/cshell/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from virtualenv.activation.via_template import ViaTemplateActivator - -if TYPE_CHECKING: - from collections.abc import Iterator - - from python_discovery import PythonInfo - - -class CShellActivator(ViaTemplateActivator): - @classmethod - def supports(cls, interpreter: PythonInfo) -> bool: - return interpreter.os != "nt" - - def templates(self) -> Iterator[str]: - yield "activate.csh" - - -__all__ = [ - "CShellActivator", -] diff --git a/src/virtualenv/activation/cshell/activate.csh b/src/virtualenv/activation/cshell/activate.csh deleted file mode 100644 index ae6fbedd8..000000000 --- a/src/virtualenv/activation/cshell/activate.csh +++ /dev/null @@ -1,74 +0,0 @@ -# This file must be used with "source bin/activate.csh" *from csh*. -# You cannot run it directly. -# Created by Davide Di Blasi . - -set newline='\ -' - -alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH:q" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_TCL_LIBRARY != 0 && setenv TCL_LIBRARY "$_OLD_VIRTUAL_TCL_LIBRARY:q" && unset _OLD_VIRTUAL_TCL_LIBRARY || unsetenv TCL_LIBRARY; test $?_OLD_VIRTUAL_TK_LIBRARY != 0 && setenv TK_LIBRARY "$_OLD_VIRTUAL_TK_LIBRARY:q" && unset _OLD_VIRTUAL_TK_LIBRARY || unsetenv TK_LIBRARY; test $?_OLD_PKG_CONFIG_PATH != 0 && setenv PKG_CONFIG_PATH "$_OLD_PKG_CONFIG_PATH:q" && unset _OLD_PKG_CONFIG_PATH || unsetenv PKG_CONFIG_PATH; 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' - -# Unset irrelevant variables. -deactivate nondestructive - -setenv VIRTUAL_ENV __VIRTUAL_ENV__ - -set _OLD_VIRTUAL_PATH="$PATH:q" -setenv PATH "$VIRTUAL_ENV:q/"__BIN_NAME__":$PATH:q" - -if ($?PKG_CONFIG_PATH) then - set _OLD_PKG_CONFIG_PATH="$PKG_CONFIG_PATH" - setenv PKG_CONFIG_PATH "${VIRTUAL_ENV}/lib/pkgconfig:${PKG_CONFIG_PATH}" -else - setenv PKG_CONFIG_PATH "${VIRTUAL_ENV}/lib/pkgconfig" -endif - -if (__TCL_LIBRARY__ != "") then - if ($?TCL_LIBRARY) then - set _OLD_VIRTUAL_TCL_LIBRARY="$TCL_LIBRARY" - endif - setenv TCL_LIBRARY __TCL_LIBRARY__ -endif - -if (__TK_LIBRARY__ != "") then - if ($?TK_LIBRARY) then - set _OLD_VIRTUAL_TK_LIBRARY="$TK_LIBRARY" - endif - setenv TK_LIBRARY __TK_LIBRARY__ -endif - -if (__VIRTUAL_PROMPT__ != "") then - setenv VIRTUAL_ENV_PROMPT __VIRTUAL_PROMPT__ -else - setenv VIRTUAL_ENV_PROMPT "$VIRTUAL_ENV:t:q" -endif - -if ( $?VIRTUAL_ENV_DISABLE_PROMPT ) then - if ( $VIRTUAL_ENV_DISABLE_PROMPT == "" ) then - set do_prompt = "1" - else - set do_prompt = "0" - endif -else - set do_prompt = "1" -endif - -if ( $do_prompt == "1" ) then - # Could be in a non-interactive environment, - # in which case, $prompt is undefined and we wouldn't - # care about the prompt anyway. - if ( $?prompt ) then - set _OLD_VIRTUAL_PROMPT="$prompt:q" - if ( "$prompt:q" =~ *"$newline:q"* ) then - : - else - set prompt = '('"$VIRTUAL_ENV_PROMPT:q"') '"$prompt:q" - endif - endif -endif - -unset env_name -unset do_prompt - -alias pydoc python -m pydoc - -rehash diff --git a/src/virtualenv/activation/fish/__init__.py b/src/virtualenv/activation/fish/__init__.py deleted file mode 100644 index 74300db75..000000000 --- a/src/virtualenv/activation/fish/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from virtualenv.activation.via_template import ViaTemplateActivator - -if TYPE_CHECKING: - from collections.abc import Iterator - from pathlib import Path - - from virtualenv.create.creator import Creator - - -class FishActivator(ViaTemplateActivator): - def templates(self) -> Iterator[str]: - yield "activate.fish" - - def replacements(self, creator: Creator, dest_folder: Path) -> dict[str, str]: - data = super().replacements(creator, dest_folder) - data.update({ - "__TCL_LIBRARY__": getattr(creator.interpreter, "tcl_lib", None) or "", - "__TK_LIBRARY__": getattr(creator.interpreter, "tk_lib", None) or "", - }) - return data - - -__all__ = [ - "FishActivator", -] diff --git a/src/virtualenv/activation/fish/activate.fish b/src/virtualenv/activation/fish/activate.fish deleted file mode 100644 index 33a8c7fc4..000000000 --- a/src/virtualenv/activation/fish/activate.fish +++ /dev/null @@ -1,118 +0,0 @@ -# This file must be used using `source bin/activate.fish` *within a running fish ( http://fishshell.com ) session*. -# Do not run it directly. - -function deactivate -d 'Exit virtualenv mode and return to the normal environment.' - # reset old environment variables - if test -n "$_OLD_VIRTUAL_PATH" - set -gx PATH $_OLD_VIRTUAL_PATH - set -e _OLD_VIRTUAL_PATH - end - - if test -n __TCL_LIBRARY__ - if test -n "$_OLD_VIRTUAL_TCL_LIBRARY"; - set -gx TCL_LIBRARY "$_OLD_VIRTUAL_TCL_LIBRARY"; - set -e _OLD_VIRTUAL_TCL_LIBRARY; - else; - set -e TCL_LIBRARY; - end - end - if test -n __TK_LIBRARY__ - if test -n "$_OLD_VIRTUAL_TK_LIBRARY"; - set -gx TK_LIBRARY "$_OLD_VIRTUAL_TK_LIBRARY"; - set -e _OLD_VIRTUAL_TK_LIBRARY; - else; - set -e TK_LIBRARY; - end - end - - if test -n "$_OLD_PKG_CONFIG_PATH" - set -gx PKG_CONFIG_PATH "$_OLD_PKG_CONFIG_PATH" - set -e _OLD_PKG_CONFIG_PATH - end - - if test -n "$_OLD_VIRTUAL_PYTHONHOME" - set -gx PYTHONHOME "$_OLD_VIRTUAL_PYTHONHOME" - set -e _OLD_VIRTUAL_PYTHONHOME - end - - if test -n "$_OLD_FISH_PROMPT_OVERRIDE" - and functions -q _old_fish_prompt - # Set an empty local `$fish_function_path` to allow the removal of `fish_prompt` using `functions -e`. - set -l fish_function_path - - # Erase virtualenv's `fish_prompt` and restore the original. - functions -e fish_prompt - functions -c _old_fish_prompt fish_prompt - functions -e _old_fish_prompt - set -e _OLD_FISH_PROMPT_OVERRIDE - end - - set -e VIRTUAL_ENV - set -e VIRTUAL_ENV_PROMPT - - if test "$argv[1]" != 'nondestructive' - # Self-destruct! - functions -e pydoc - functions -e deactivate - end -end - -# Unset irrelevant variables. -deactivate nondestructive - -set -gx VIRTUAL_ENV __VIRTUAL_ENV__ - -set -gx _OLD_PKG_CONFIG_PATH "$PKG_CONFIG_PATH" -set -gx PKG_CONFIG_PATH "$VIRTUAL_ENV/lib/pkgconfig:$PKG_CONFIG_PATH" - -set -gx _OLD_VIRTUAL_PATH $PATH -set -gx PATH "$VIRTUAL_ENV"'/'__BIN_NAME__ $PATH - -if test -n __TCL_LIBRARY__ - if set -q TCL_LIBRARY; - set -gx _OLD_VIRTUAL_TCL_LIBRARY $TCL_LIBRARY; - end - set -gx TCL_LIBRARY '__TCL_LIBRARY__' -end -if test -n __TK_LIBRARY__ - if set -q TK_LIBRARY; - set -gx _OLD_VIRTUAL_TK_LIBRARY $TK_LIBRARY; - end - set -gx TK_LIBRARY '__TK_LIBRARY__' -end - -# Prompt override provided? -# If not, just use the environment name. -if test -n __VIRTUAL_PROMPT__ - set -gx VIRTUAL_ENV_PROMPT __VIRTUAL_PROMPT__ -else - set -gx VIRTUAL_ENV_PROMPT (basename "$VIRTUAL_ENV") -end - -# Unset `$PYTHONHOME` if set. -if set -q PYTHONHOME - set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME - set -e PYTHONHOME -end - -function pydoc - python -m pydoc $argv -end - -if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" - # Copy the current `fish_prompt` function as `_old_fish_prompt`. - functions -c fish_prompt _old_fish_prompt - - function fish_prompt - set -l old_status $status - # Run the user's prompt first; it might depend on (pipe)status. - set -l prompt (_old_fish_prompt) - - printf '(%s) ' $VIRTUAL_ENV_PROMPT - - string join -- \n $prompt # handle multi-line prompts - echo "exit $old_status" | . - end - - set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" -end diff --git a/src/virtualenv/activation/nushell/__init__.py b/src/virtualenv/activation/nushell/__init__.py deleted file mode 100644 index 60a0d78be..000000000 --- a/src/virtualenv/activation/nushell/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from virtualenv.activation.via_template import ViaTemplateActivator - -if TYPE_CHECKING: - from collections.abc import Iterator - from pathlib import Path - - from virtualenv.create.creator import Creator - - -class NushellActivator(ViaTemplateActivator): - def templates(self) -> Iterator[str]: - yield "activate.nu" - - @staticmethod - def quote(string: str) -> str: - """Nushell supports raw strings like: r###'this is a string'###. - - https://github.com/nushell/nushell.github.io/blob/main/book/working_with_strings.md - - This method finds the maximum continuous sharps in the string and then quote it with an extra sharp. - - """ - max_sharps = 0 - current_sharps = 0 - for char in string: - if char == "#": - current_sharps += 1 - max_sharps = max(current_sharps, max_sharps) - else: - current_sharps = 0 - wrapping = "#" * (max_sharps + 1) - return f"r{wrapping}'{string}'{wrapping}" - - def replacements(self, creator: Creator, dest_folder: Path) -> dict[str, str]: # noqa: ARG002 - return { - "__VIRTUAL_PROMPT__": "" if self.flag_prompt is None else self.flag_prompt, - "__VIRTUAL_ENV__": str(creator.dest), - "__VIRTUAL_NAME__": creator.env_name, - "__BIN_NAME__": str(creator.bin_dir.relative_to(creator.dest)), - "__TCL_LIBRARY__": getattr(creator.interpreter, "tcl_lib", None) or "", - "__TK_LIBRARY__": getattr(creator.interpreter, "tk_lib", None) or "", - } - - -__all__ = [ - "NushellActivator", -] diff --git a/src/virtualenv/activation/nushell/activate.nu b/src/virtualenv/activation/nushell/activate.nu deleted file mode 100644 index 7046c5880..000000000 --- a/src/virtualenv/activation/nushell/activate.nu +++ /dev/null @@ -1,89 +0,0 @@ -# virtualenv activation module: -# - Activate with `overlay use activate.nu` -# - Deactivate with `deactivate`, as usual -# -# To customize the overlay name, you can call `overlay use activate.nu as foo`, but then simply `deactivate` won't work -# because it is just an alias to hide the "activate" overlay. You'd need to call `overlay hide foo` manually. - -module warning { - export-env { - const file = path self - error make -u { - msg: $"`($file | path basename)` is meant to be used with `overlay use`, not `source`" - } - } - -} - -use warning - -export-env { - - let nu_ver = (version | get version | split row '.' | take 2 | each { into int }) - if $nu_ver.0 == 0 and $nu_ver.1 < 106 { - error make { - msg: 'virtualenv Nushell activation requires Nushell 0.106 or greater.' - } - } - - def is-string [x] { - ($x | describe) == 'string' - } - - def has-env [...names] { - $names | each {|n| $n in $env } | all {|i| $i } - } - - def is-env-true [name: string] { - if (has-env $name) { - let val = ($env | get --optional $name) - if ($val | describe) == 'bool' { - $val - } else { - not ($val | is-empty) - } - } else { - false - } - } - - let virtual_env = __VIRTUAL_ENV__ - let bin = __BIN_NAME__ - let path_name = if (has-env 'Path') { 'Path' } else { 'PATH' } - let venv_path = ([$virtual_env $bin] | path join) - let new_path = ($env | get $path_name | prepend $venv_path) - let virtual_env_prompt = if (__VIRTUAL_PROMPT__ | is-empty) { - ($virtual_env | path basename) - } else { - __VIRTUAL_PROMPT__ - } - let old_pkg_config_path = if (has-env 'PKG_CONFIG_PATH') { $env.PKG_CONFIG_PATH } else { '' } - let new_pkg_config_path = $'($virtual_env)/lib/pkgconfig:($old_pkg_config_path)' - let new_env = { $path_name: $new_path VIRTUAL_ENV: $virtual_env VIRTUAL_ENV_PROMPT: $virtual_env_prompt PKG_CONFIG_PATH: $new_pkg_config_path } - if (has-env 'TCL_LIBRARY') { - let $new_env = $new_env | insert TCL_LIBRARY __TCL_LIBRARY__ - } - if (has-env 'TK_LIBRARY') { - let $new_env = $new_env | insert TK_LIBRARY __TK_LIBRARY__ - } - let old_prompt_command = if (has-env 'PROMPT_COMMAND') { $env.PROMPT_COMMAND } else { '' } - let new_env = if (is-env-true 'VIRTUAL_ENV_DISABLE_PROMPT') { - $new_env - } else { - let virtual_prefix = $'(char lparen)($virtual_env_prompt)(char rparen) ' - let new_prompt = if (has-env 'PROMPT_COMMAND') { - if ('closure' in ($old_prompt_command | describe)) { - {|| $'($virtual_prefix)(do $old_prompt_command)' } - } else { - {|| $'($virtual_prefix)($old_prompt_command)' } - } - } else { - {|| $'($virtual_prefix)' } - } - $new_env | merge { PROMPT_COMMAND: $new_prompt VIRTUAL_PREFIX: $virtual_prefix } - } - load-env $new_env -} - -export alias pydoc = python -m pydoc -export alias deactivate = overlay hide activate diff --git a/src/virtualenv/activation/powershell/__init__.py b/src/virtualenv/activation/powershell/__init__.py deleted file mode 100644 index 7efe907c6..000000000 --- a/src/virtualenv/activation/powershell/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from virtualenv.activation.via_template import ViaTemplateActivator - -if TYPE_CHECKING: - from collections.abc import Iterator - - -class PowerShellActivator(ViaTemplateActivator): - def templates(self) -> Iterator[str]: - yield "activate.ps1" - - @staticmethod - def quote(string: str) -> str: - """This should satisfy PowerShell quoting rules [1], unless the quoted string is passed directly to Windows native commands [2]. - - [1]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules - [2]: - https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing#passing-arguments-that-contain-quote-characters - - """ - string = string.replace("'", "''") - return f"'{string}'" - - -__all__ = [ - "PowerShellActivator", -] diff --git a/src/virtualenv/activation/powershell/activate.ps1 b/src/virtualenv/activation/powershell/activate.ps1 deleted file mode 100644 index e56633a5b..000000000 --- a/src/virtualenv/activation/powershell/activate.ps1 +++ /dev/null @@ -1,213 +0,0 @@ -<# -.Synopsis -Activate a Python virtual environment for the current PowerShell session. - -.Description -Pushes the python executable for a virtual environment to the front of the -$Env:PATH environment variable and sets the prompt to signify that you are -in a Python virtual environment. Makes use of the command line switches as -well as the `pyvenv.cfg` file values present in the virtual environment. - -.Parameter VenvDir -Path to the directory that contains the virtual environment to activate. The -default value for this is the parent of the directory that the activate.ps1 -script is located within. - -.Parameter Prompt -The prompt prefix to display when this virtual environment is activated. By -default, this prompt is the name of the virtual environment folder (VenvDir) -surrounded by parentheses and followed by a single space (ie. '(.venv) '). - -.Example -activate.ps1 -Activates the Python virtual environment that contains the activate.ps1 script. - -.Example -activate.ps1 -Verbose -Activates the Python virtual environment that contains the activate.ps1 script, -and shows extra information about the activation as it executes. - -.Example -activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv -Activates the Python virtual environment located in the specified location. - -.Example -activate.ps1 -Prompt "MyPython" -Activates the Python virtual environment that contains the activate.ps1 script, -and prefixes the current prompt with the specified string (surrounded in -parentheses) while the virtual environment is active. - -.Notes -On Windows, it may be required to enable this activate.ps1 script by setting the -execution policy for the user. You can do this by issuing the following PowerShell -command: - -PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser - -For more information on Execution Policies: -https://go.microsoft.com/fwlink/?LinkID=135170 - -#> -Param( - [Parameter(Mandatory = $false)] - [String] - $VenvDir, - [Parameter(Mandatory = $false)] - [String] - $Prompt -) - -function Get-PyVenvConfig( - [String] - $ConfigDir -) { - Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" - - $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue - - $pyvenvConfig = @{ } - - if ($pyvenvConfigPath) { - Write-Verbose "File exists, parse ``key = value`` lines" - $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath - - $pyvenvConfigContent | ForEach-Object { - $keyval = $PSItem -split "\s*=\s*", 2 - if ($keyval[0] -and $keyval[1]) { - $val = $keyval[1] - - if ("'""".Contains($val.Substring(0, 1))) { - $val = $val.Substring(1, $val.Length - 2) - } - - $pyvenvConfig[$keyval[0]] = $val - Write-Verbose "Adding Key: '$($keyval[0])'='$val'" - } - } - } - return $pyvenvConfig -} - -function global:deactivate([switch] $NonDestructive) { - if (Test-Path variable:_OLD_VIRTUAL_PATH) { - $env:PATH = $variable:_OLD_VIRTUAL_PATH - Remove-Variable "_OLD_VIRTUAL_PATH" -Scope global - } - - if (Test-Path variable:_OLD_VIRTUAL_TCL_LIBRARY) { - $env:TCL_LIBRARY = $variable:_OLD_VIRTUAL_TCL_LIBRARY - Remove-Variable "_OLD_VIRTUAL_TCL_LIBRARY" -Scope global - } else { - if (Test-Path env:TCL_LIBRARY) { - Remove-Item env:TCL_LIBRARY -ErrorAction SilentlyContinue - } - } - - if (Test-Path variable:_OLD_VIRTUAL_TK_LIBRARY) { - $env:TK_LIBRARY = $variable:_OLD_VIRTUAL_TK_LIBRARY - Remove-Variable "_OLD_VIRTUAL_TK_LIBRARY" -Scope global - } else { - if (Test-Path env:TK_LIBRARY) { - Remove-Item env:TK_LIBRARY -ErrorAction SilentlyContinue - } - } - - if (Test-Path variable:_OLD_PKG_CONFIG_PATH) { - $env:PKG_CONFIG_PATH = $variable:_OLD_PKG_CONFIG_PATH - Remove-Variable "_OLD_PKG_CONFIG_PATH" -Scope global - } - - if (Test-Path function:_old_virtual_prompt) { - $function:prompt = $function:_old_virtual_prompt - Remove-Item function:\_old_virtual_prompt - } - - if ($env:VIRTUAL_ENV) { - Remove-Item env:VIRTUAL_ENV -ErrorAction SilentlyContinue - } - - if ($env:VIRTUAL_ENV_PROMPT) { - Remove-Item env:VIRTUAL_ENV_PROMPT -ErrorAction SilentlyContinue - } - - if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { - Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force - } - - if (!$NonDestructive) { - Remove-Item function:deactivate - Remove-Item function:pydoc - } -} - -function global:pydoc { - & python -m pydoc $args -} - -$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition -$VenvExecDir = Get-Item -Path $VenvExecPath - -Write-Verbose "Activation script is located in path: '$VenvExecPath'" - -if ($VenvDir) { - Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" -} else { - $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") - Write-Verbose "VenvDir=$VenvDir" -} - -$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir - -if ($Prompt) { - Write-Verbose "Prompt specified as argument, using '$Prompt'" -} else { - if ($pyvenvCfg -and $pyvenvCfg['prompt']) { - Write-Verbose "Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" - $Prompt = $pyvenvCfg['prompt'] - } elseif (__VIRTUAL_PROMPT__ -ne "") { - $Prompt = __VIRTUAL_PROMPT__ - } else { - $Prompt = Split-Path -Path $VenvDir -Leaf - } -} - -deactivate -nondestructive - -$env:VIRTUAL_ENV = $VenvDir -$env:VIRTUAL_ENV_PROMPT = $Prompt - -if (__TCL_LIBRARY__ -ne "") { - if (Test-Path env:TCL_LIBRARY) { - New-Variable -Scope global -Name _OLD_VIRTUAL_TCL_LIBRARY -Value $env:TCL_LIBRARY - } - $env:TCL_LIBRARY = __TCL_LIBRARY__ -} - -if (__TK_LIBRARY__ -ne "") { - if (Test-Path env:TK_LIBRARY) { - New-Variable -Scope global -Name _OLD_VIRTUAL_TK_LIBRARY -Value $env:TK_LIBRARY - } - $env:TK_LIBRARY = __TK_LIBRARY__ -} - -New-Variable -Scope global -Name _OLD_VIRTUAL_PATH -Value $env:PATH - -if (Test-Path env:PKG_CONFIG_PATH) { - New-Variable -Scope global -Name _OLD_PKG_CONFIG_PATH -Value $env:PKG_CONFIG_PATH -} -$env:PKG_CONFIG_PATH = "$env:VIRTUAL_ENV\lib\pkgconfig;$env:PKG_CONFIG_PATH" - -$env:PATH = "$env:VIRTUAL_ENV/" + __BIN_NAME__ + __PATH_SEP__ + $env:PATH - -if (!$env:VIRTUAL_ENV_DISABLE_PROMPT) { - function global:_old_virtual_prompt { - "" - } - $function:_old_virtual_prompt = $function:prompt - New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt - - function global:prompt { - Write-Host -NoNewline -ForegroundColor Green "($($_PYTHON_VENV_PROMPT_PREFIX)) " - _old_virtual_prompt - } -} diff --git a/src/virtualenv/activation/python/__init__.py b/src/virtualenv/activation/python/__init__.py deleted file mode 100644 index ef59448b4..000000000 --- a/src/virtualenv/activation/python/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import annotations - -import os -from collections import OrderedDict -from typing import TYPE_CHECKING - -from virtualenv.activation.via_template import ViaTemplateActivator - -if TYPE_CHECKING: - from collections.abc import Iterator - from pathlib import Path - - from virtualenv.create.creator import Creator - - -class PythonActivator(ViaTemplateActivator): - def templates(self) -> Iterator[str]: - yield "activate_this.py" - - @staticmethod - def quote(string: str) -> str: - return repr(string) - - def replacements(self, creator: Creator, dest_folder: Path) -> dict[str, str]: - replacements = super().replacements(creator, dest_folder) - lib_folders = OrderedDict((os.path.relpath(str(i), str(dest_folder)), None) for i in creator.libs) - lib_folders = os.pathsep.join(lib_folders.keys()) - replacements.update( - { - "__LIB_FOLDERS__": lib_folders, - "__DECODE_PATH__": "", - }, - ) - return replacements - - -__all__ = [ - "PythonActivator", -] diff --git a/src/virtualenv/activation/python/activate_this.py b/src/virtualenv/activation/python/activate_this.py deleted file mode 100644 index 69a4b67c8..000000000 --- a/src/virtualenv/activation/python/activate_this.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Activate virtualenv for current interpreter: - -import runpy runpy.run_path(this_file) - -This can be used when you must use an existing Python interpreter, not the virtualenv bin/python. - -""" # noqa: D415 - -from __future__ import annotations - -import os -import site -import sys - -try: - abs_file = os.path.abspath(__file__) -except NameError as exc: - msg = "You must use import runpy; runpy.run_path(this_file)" - raise AssertionError(msg) from exc - -bin_dir = os.path.dirname(abs_file) -base = bin_dir[: -len(__BIN_NAME__) - 1] # ty: ignore[unresolved-reference] - -# prepend bin to PATH (this file is inside the bin directory) -os.environ["PATH"] = os.pathsep.join([bin_dir, *os.environ.get("PATH", "").split(os.pathsep)]) -os.environ["VIRTUAL_ENV"] = base # virtual env is right above bin directory -os.environ["VIRTUAL_ENV_PROMPT"] = __VIRTUAL_PROMPT__ or os.path.basename(base) # ty: ignore[unresolved-reference] - -# Set PKG_CONFIG_PATH to include the virtualenv's pkgconfig directory -pkg_config_path = os.path.join(base, "lib", "pkgconfig") -existing_pkg_config_path = os.environ.get("PKG_CONFIG_PATH", "") -if existing_pkg_config_path: - os.environ["PKG_CONFIG_PATH"] = os.pathsep.join([pkg_config_path, existing_pkg_config_path]) -else: - os.environ["PKG_CONFIG_PATH"] = pkg_config_path - -# add the virtual environments libraries to the host python import mechanism -prev_length = len(sys.path) -for lib in __LIB_FOLDERS__.split(os.pathsep): # ty: ignore[unresolved-reference] - path = os.path.realpath(os.path.join(bin_dir, lib)) - site.addsitedir(path.decode("utf-8") if __DECODE_PATH__ else path) # ty: ignore[unresolved-reference,unresolved-attribute] -sys.path[:] = sys.path[prev_length:] + sys.path[0:prev_length] - -sys.real_prefix = sys.prefix # ty: ignore[unresolved-attribute] -sys.prefix = base diff --git a/src/virtualenv/activation/via_template.py b/src/virtualenv/activation/via_template.py deleted file mode 100644 index 926c2b0be..000000000 --- a/src/virtualenv/activation/via_template.py +++ /dev/null @@ -1,99 +0,0 @@ -from __future__ import annotations - -import os -import shlex -import sys -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING - -from .activator import Activator - -if TYPE_CHECKING: - from collections.abc import Iterable, Iterator - from pathlib import Path - - from virtualenv.create.creator import Creator - -if sys.version_info >= (3, 10): - from importlib.resources import files - - def read_binary(module_name: str, filename: str) -> bytes: - return (files(module_name) / filename).read_bytes() - -else: - from importlib.resources import read_binary - - -class ViaTemplateActivator(Activator, ABC): - @abstractmethod - def templates(self) -> Iterator[str]: - raise NotImplementedError - - @staticmethod - def quote(string: str) -> str: - """Quote strings in the activation script. - - :param string: the string to quote - - :returns: quoted string that works in the activation script - - """ - return shlex.quote(string) - - def generate(self, creator: Creator) -> list[Path]: - dest_folder = creator.bin_dir - replacements = self.replacements(creator, dest_folder) - generated = self._generate(replacements, self.templates(), dest_folder, creator) - if self.flag_prompt is not None: - creator.pyenv_cfg["prompt"] = self.flag_prompt - return generated - - def replacements(self, creator: Creator, dest_folder: Path) -> dict[str, str]: # noqa: ARG002 - return { - "__VIRTUAL_PROMPT__": "" if self.flag_prompt is None else self.flag_prompt, - "__VIRTUAL_ENV__": str(creator.dest), - "__VIRTUAL_NAME__": creator.env_name, - "__BIN_NAME__": str(creator.bin_dir.relative_to(creator.dest)), - "__PATH_SEP__": os.pathsep, - "__TCL_LIBRARY__": getattr(creator.interpreter, "tcl_lib", None) or "", - "__TK_LIBRARY__": getattr(creator.interpreter, "tk_lib", None) or "", - } - - def _generate( - self, replacements: dict[str, str], templates: Iterable[str], to_folder: Path, creator: Creator - ) -> list[Path]: - generated = [] - for template in templates: - text = self.instantiate_template(replacements, template, creator) - dest = to_folder / self.as_name(template) - # remove the file if it already exists - this prevents permission - # errors when the dest is not writable - if dest.exists(): - dest.unlink() - # Powershell assumes Windows 1252 encoding when reading files without BOM - encoding = "utf-8-sig" if str(template).endswith(".ps1") else "utf-8" - # use write_bytes to avoid platform specific line normalization (\n -> \r\n) - dest.write_bytes(text.encode(encoding)) - generated.append(dest) - return generated - - def as_name(self, template: str) -> str: - return template - - def instantiate_template(self, replacements: dict[str, str], template: str, creator: Creator) -> str: - # read content as binary to avoid platform specific line normalization (\n -> \r\n) - binary = read_binary(self.__module__, template) - text = binary.decode("utf-8", errors="strict") - for key, value in replacements.items(): - value_uni = self._repr_unicode(creator, value) - text = text.replace(key, self.quote(value_uni)) - return text - - @staticmethod - def _repr_unicode(creator: Creator, value: str) -> str: # noqa: ARG004 - return value # by default, we just let it be unicode - - -__all__ = [ - "ViaTemplateActivator", -] diff --git a/src/virtualenv/app_data/__init__.py b/src/virtualenv/app_data/__init__.py deleted file mode 100644 index 576b56651..000000000 --- a/src/virtualenv/app_data/__init__.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Application data stored by virtualenv.""" - -from __future__ import annotations - -import logging -import os -import shutil -from typing import TYPE_CHECKING, Any - -from platformdirs import user_cache_dir, user_data_dir - -from .na import AppDataDisabled -from .read_only import ReadOnlyAppData -from .via_disk_folder import AppDataDiskFolder -from .via_tempdir import TempAppData - -if TYPE_CHECKING: - from collections.abc import Mapping - - from .base import AppData - -LOGGER = logging.getLogger(__name__) - - -def _default_app_data_dir(env: Mapping[str, str]) -> str: - key = "VIRTUALENV_OVERRIDE_APP_DATA" - if key in env: - return env[key] - return _cache_dir_with_migration() - - -def _cache_dir_with_migration() -> str: - new_dir = user_cache_dir(appname="virtualenv", appauthor="pypa") - old_dir = user_data_dir(appname="virtualenv", appauthor="pypa") - if new_dir == old_dir: - return new_dir - if os.path.isdir(old_dir) and not os.path.isdir(new_dir): - LOGGER.info("migrating app data from %s to %s", old_dir, new_dir) - try: - shutil.move(old_dir, new_dir) - except OSError as exception: - LOGGER.warning( - "could not migrate app data from %s to %s: %r, using old location", old_dir, new_dir, exception - ) - return old_dir - return new_dir - - -def make_app_data(folder: str | None, **kwargs: Any) -> AppData: # noqa: ANN401 - is_read_only = kwargs.pop("read_only") - env = kwargs.pop("env") - if kwargs: # py3+ kwonly - msg = "unexpected keywords: {}" - raise TypeError(msg) - - if folder is None: - folder = _default_app_data_dir(env) - folder = os.path.abspath(folder) - - if is_read_only: - return ReadOnlyAppData(folder) - - try: - os.makedirs(folder, exist_ok=True) - LOGGER.debug("created app data folder %s", folder) - except OSError as exception: - LOGGER.info("could not create app data folder %s due to %r", folder, exception) - - if os.access(folder, os.W_OK): - return AppDataDiskFolder(folder) - LOGGER.debug("app data folder %s has no write access", folder) - return TempAppData() - - -__all__ = ( - "AppDataDisabled", - "AppDataDiskFolder", - "ReadOnlyAppData", - "TempAppData", - "make_app_data", -) diff --git a/src/virtualenv/app_data/base.py b/src/virtualenv/app_data/base.py deleted file mode 100644 index b18b96b00..000000000 --- a/src/virtualenv/app_data/base.py +++ /dev/null @@ -1,167 +0,0 @@ -"""Application data stored by virtualenv.""" - -from __future__ import annotations - -from abc import ABC, abstractmethod -from contextlib import contextmanager -from typing import TYPE_CHECKING - -from virtualenv.info import IS_ZIPAPP - -if TYPE_CHECKING: - from collections.abc import Generator - from pathlib import Path - from typing import Any - - -class AppData(ABC): - """Abstract storage interface for the virtualenv application.""" - - @abstractmethod - def close(self) -> None: - """Called before virtualenv exits.""" - - @abstractmethod - def reset(self) -> None: - """Called when the user passes in the reset app data.""" - - @abstractmethod - def py_info(self, path: Path) -> ContentStore: - """Return a content store for cached interpreter information at the given path. - - :param path: the interpreter executable path - - :returns: a content store for the cached data - - """ - raise NotImplementedError - - @abstractmethod - def py_info_clear(self) -> None: - """Clear all cached interpreter information.""" - raise NotImplementedError - - @property - def can_update(self) -> bool: - """``True`` if this app data store supports updating cached content.""" - raise NotImplementedError - - @abstractmethod - def embed_update_log(self, distribution: str, for_py_version: str) -> ContentStore: - """Return a content store for the embed update log of a distribution. - - :param distribution: the package name (e.g. ``pip``) - :param for_py_version: the target Python version string - - :returns: a content store for the update log - - """ - raise NotImplementedError - - @property - def house(self) -> Path: - """The root directory of the application data store.""" - raise NotImplementedError - - @property - def transient(self) -> bool: - """``True`` if this app data store is transient and does not persist across runs.""" - raise NotImplementedError - - @abstractmethod - def wheel_image(self, for_py_version: str, name: str) -> Path: - """Return the path to a cached wheel image. - - :param for_py_version: the target Python version string - :param name: the package name - - :returns: the path to the cached wheel - - """ - raise NotImplementedError - - @contextmanager - def ensure_extracted(self, path: Path, to_folder: Path | None = None) -> Generator[Path]: - """Ensure a path is available on disk, extracting from zipapp if needed. - - :param path: the path to ensure is available - :param to_folder: optional target directory for extraction - - :returns: yields the usable path on disk - - """ - if IS_ZIPAPP: - with self.extract(path, to_folder) as result: - yield result - else: - yield path - - @abstractmethod - @contextmanager - def extract(self, path: Path, to_folder: Path | None) -> Generator[Path]: - """Extract a path from the zipapp to a location on disk. - - :param path: the path to extract - :param to_folder: optional target directory - - :returns: yields the extracted path - - """ - raise NotImplementedError - - @abstractmethod - @contextmanager - def locked(self, path: Path) -> Generator[None]: - """Acquire an exclusive lock on the given path. - - :param path: the path to lock - - """ - raise NotImplementedError - - -class ContentStore(ABC): - """A store for reading and writing cached content.""" - - @abstractmethod - def exists(self) -> bool: - """Check if the stored content exists. - - :returns: ``True`` if content exists - - """ - raise NotImplementedError - - @abstractmethod - def read(self) -> Any: # noqa: ANN401 - """Read the stored content. - - :returns: the stored content - - """ - raise NotImplementedError - - @abstractmethod - def write(self, content: Any) -> None: # noqa: ANN401 - """Write content to the store. - - :param content: the content to write - - """ - raise NotImplementedError - - @abstractmethod - def remove(self) -> None: - """Remove the stored content.""" - raise NotImplementedError - - @abstractmethod - @contextmanager - def locked(self) -> Generator[None]: - """Acquire an exclusive lock on this content store.""" - - -__all__ = [ - "AppData", - "ContentStore", -] diff --git a/src/virtualenv/app_data/na.py b/src/virtualenv/app_data/na.py deleted file mode 100644 index e6170f879..000000000 --- a/src/virtualenv/app_data/na.py +++ /dev/null @@ -1,78 +0,0 @@ -from __future__ import annotations - -from contextlib import contextmanager -from typing import TYPE_CHECKING - -from .base import AppData, ContentStore - -if TYPE_CHECKING: - from collections.abc import Generator - from pathlib import Path - from typing import Any, NoReturn - - -class AppDataDisabled(AppData): - """No application cache available (most likely as we don't have write permissions).""" - - transient = True - can_update = False - - def __init__(self) -> None: - pass - - error = RuntimeError("no app data folder available, probably no write access to the folder") - - def close(self) -> None: - """Do nothing.""" - - def reset(self) -> None: - """Do nothing.""" - - def py_info(self, path: Path) -> ContentStoreNA: # noqa: ARG002 - return ContentStoreNA() - - def embed_update_log(self, distribution: str, for_py_version: str) -> ContentStoreNA: # noqa: ARG002 - return ContentStoreNA() - - def extract(self, path: Path, to_folder: Path | None) -> NoReturn: # noqa: ARG002 - raise self.error - - @contextmanager - def locked(self, path: Path) -> Generator[None]: # noqa: ARG002 - """Do nothing.""" - yield - - @property - def house(self) -> NoReturn: - raise self.error - - def wheel_image(self, for_py_version: str, name: str) -> NoReturn: # noqa: ARG002 - raise self.error - - def py_info_clear(self) -> None: - """Nothing to clear.""" - - -class ContentStoreNA(ContentStore): - def exists(self) -> bool: - return False - - def read(self) -> None: - """Nothing to read.""" - return - - def write(self, content: Any) -> None: # noqa: ANN401 - """Nothing to write.""" - - def remove(self) -> None: - """Nothing to remove.""" - - @contextmanager - def locked(self) -> Generator[None]: - yield - - -__all__ = [ - "AppDataDisabled", - "ContentStoreNA", -] diff --git a/src/virtualenv/app_data/read_only.py b/src/virtualenv/app_data/read_only.py deleted file mode 100644 index 5bd62c7f5..000000000 --- a/src/virtualenv/app_data/read_only.py +++ /dev/null @@ -1,47 +0,0 @@ -from __future__ import annotations - -import os.path -from typing import TYPE_CHECKING - -from virtualenv.util.lock import NoOpFileLock - -from .via_disk_folder import AppDataDiskFolder, PyInfoStoreDisk - -if TYPE_CHECKING: - from pathlib import Path - from typing import NoReturn - - -class ReadOnlyAppData(AppDataDiskFolder): - can_update = False - - def __init__(self, folder: str) -> None: - if not os.path.isdir(folder): - msg = f"read-only app data directory {folder} does not exist" - raise RuntimeError(msg) - super().__init__(folder) - self.lock = NoOpFileLock(folder) - - def reset(self) -> None: - msg = "read-only app data does not support reset" - raise RuntimeError(msg) - - def py_info_clear(self) -> None: - raise NotImplementedError - - def py_info(self, path: Path) -> _PyInfoStoreDiskReadOnly: - return _PyInfoStoreDiskReadOnly(self.py_info_at, path) - - def embed_update_log(self, distribution: str, for_py_version: str) -> NoReturn: - raise NotImplementedError - - -class _PyInfoStoreDiskReadOnly(PyInfoStoreDisk): - def write(self, content: str) -> NoReturn: # noqa: ARG002 - msg = "read-only app data python info cannot be updated" - raise RuntimeError(msg) - - -__all__ = [ - "ReadOnlyAppData", -] diff --git a/src/virtualenv/app_data/via_disk_folder.py b/src/virtualenv/app_data/via_disk_folder.py deleted file mode 100644 index 5d7500203..000000000 --- a/src/virtualenv/app_data/via_disk_folder.py +++ /dev/null @@ -1,181 +0,0 @@ -r"""A rough layout of the current storage goes as: - -:: - - virtualenv-app-data - โ”œโ”€โ”€ py - - โ”‚ โ””โ”€โ”€ *.json/lock - โ”œโ”€โ”€ wheel - โ”‚ โ”œโ”€โ”€ house - โ”‚ โ”‚ โ””โ”€โ”€ *.whl - โ”‚ โ””โ”€โ”€ -> 3.9 - โ”‚ โ”œโ”€โ”€ img- - โ”‚ โ”‚ โ””โ”€โ”€ image - โ”‚ โ”‚ โ””โ”€โ”€ -> CopyPipInstall / SymlinkPipInstall - โ”‚ โ”‚ โ””โ”€โ”€ -> pip-20.1.1-py2.py3-none-any - โ”‚ โ””โ”€โ”€ embed - โ”‚ โ””โ”€โ”€ 3 -> json format versioning - โ”‚ โ””โ”€โ”€ *.json -> for every distribution contains data about newer embed versions and releases - โ””โ”€โ”€โ”€ unzip - โ””โ”€โ”€ - โ”œโ”€โ”€ py_info.py - โ”œโ”€โ”€ debug.py - โ””โ”€โ”€ _virtualenv.py - -""" # noqa: D415 - -from __future__ import annotations - -import json -import logging -from abc import ABC -from contextlib import contextmanager, suppress -from hashlib import sha256 -from typing import TYPE_CHECKING, Any - -from virtualenv.util.lock import ReentrantFileLock -from virtualenv.util.path import safe_delete -from virtualenv.util.zipapp import extract -from virtualenv.version import __version__ - -from .base import AppData, ContentStore - -if TYPE_CHECKING: - from collections.abc import Generator - from pathlib import Path - -LOGGER = logging.getLogger(__name__) - - -class AppDataDiskFolder(AppData): - """Store the application data on the disk within a folder layout.""" - - transient = False - can_update = True - - def __init__(self, folder: str) -> None: - self.lock = ReentrantFileLock(folder) - - def __repr__(self) -> str: - return f"{type(self).__name__}({self.lock.path})" - - def __str__(self) -> str: - return str(self.lock.path) - - def reset(self) -> None: - LOGGER.debug("reset app data folder %s", self.lock.path) - safe_delete(self.lock.path) - - def close(self) -> None: - """Do nothing.""" - - @contextmanager - def locked(self, path: Path) -> Generator[None]: - path_lock = self.lock / path # ty: ignore[unsupported-operator] - with path_lock: - yield path_lock.path - - @contextmanager - def extract(self, path: Path, to_folder: Path | None) -> Generator[Path]: - root = ReentrantFileLock(to_folder()) if to_folder is not None else self.lock / "unzip" / __version__ # ty: ignore[call-non-callable] - with root.lock_for_key(path.name): - dest = root.path / path.name - if not dest.exists(): - extract(path, dest) - yield dest - - @property - def py_info_at(self) -> ReentrantFileLock: - return self.lock / "py_info" / "4" # ty: ignore[invalid-return-type] - - def py_info(self, path: Path) -> PyInfoStoreDisk: - return PyInfoStoreDisk(self.py_info_at, path) - - def py_info_clear(self) -> None: - """clear py info.""" - py_info_folder = self.py_info_at - with py_info_folder: - for filename in py_info_folder.path.iterdir(): - if filename.suffix == ".json": - with py_info_folder.lock_for_key(filename.stem): - if filename.exists(): - filename.unlink() - - def embed_update_log(self, distribution: str, for_py_version: str) -> EmbedDistributionUpdateStoreDisk: - return EmbedDistributionUpdateStoreDisk(self.lock / "wheel" / for_py_version / "embed" / "3", distribution) # ty: ignore[invalid-argument-type] - - @property - def house(self) -> Path: - path = self.lock.path / "wheel" / "house" - path.mkdir(parents=True, exist_ok=True) - return path - - def wheel_image(self, for_py_version: str, name: str) -> Path: - return self.lock.path / "wheel" / for_py_version / "image" / "1" / name - - -class JSONStoreDisk(ContentStore, ABC): - def __init__(self, in_folder: ReentrantFileLock, key: str, msg_args: tuple[str, ...]) -> None: - self.in_folder = in_folder - self.key = key - self.msg_args = (*msg_args, self.file) - - @property - def file(self) -> Path: - return self.in_folder.path / f"{self.key}.json" - - def exists(self) -> bool: - return self.file.exists() - - def read(self) -> Any: # noqa: ANN401 - data, bad_format = None, False - try: - data = json.loads(self.file.read_text(encoding="utf-8")) - except ValueError: - bad_format = True - except Exception: # noqa: BLE001, S110 - pass - else: - LOGGER.debug("got %s %s from %s", *self.msg_args) - return data - if bad_format: - with suppress(OSError): # reading and writing on the same file may cause race on multiple processes - self.remove() - return None - - def remove(self) -> None: - self.file.unlink() - LOGGER.debug("removed %s %s at %s", *self.msg_args) - - @contextmanager - def locked(self) -> Generator[None]: - with self.in_folder.lock_for_key(self.key): - yield - - def write(self, content: Any) -> None: # noqa: ANN401 - folder = self.file.parent - folder.mkdir(parents=True, exist_ok=True) - self.file.write_text(json.dumps(content, sort_keys=True, indent=2), encoding="utf-8") - LOGGER.debug("wrote %s %s at %s", *self.msg_args) - - -class PyInfoStoreDisk(JSONStoreDisk): - def __init__(self, in_folder: ReentrantFileLock, path: Path) -> None: - key = sha256(str(path).encode("utf-8")).hexdigest() - super().__init__(in_folder, key, ("python info of", path)) # ty: ignore[invalid-argument-type] - - -class EmbedDistributionUpdateStoreDisk(JSONStoreDisk): - def __init__(self, in_folder: ReentrantFileLock, distribution: str) -> None: - super().__init__( - in_folder, - distribution, - ("embed update of distribution", distribution), - ) - - -__all__ = [ - "AppDataDiskFolder", - "JSONStoreDisk", - "PyInfoStoreDisk", -] diff --git a/src/virtualenv/app_data/via_tempdir.py b/src/virtualenv/app_data/via_tempdir.py deleted file mode 100644 index a690f3c79..000000000 --- a/src/virtualenv/app_data/via_tempdir.py +++ /dev/null @@ -1,38 +0,0 @@ -from __future__ import annotations - -import logging -from tempfile import mkdtemp -from typing import TYPE_CHECKING - -from virtualenv.util.path import safe_delete - -from .via_disk_folder import AppDataDiskFolder - -if TYPE_CHECKING: - from typing import NoReturn - -LOGGER = logging.getLogger(__name__) - - -class TempAppData(AppDataDiskFolder): - transient = True - can_update = False - - def __init__(self) -> None: - super().__init__(folder=mkdtemp()) - LOGGER.debug("created temporary app data folder %s", self.lock.path) - - def reset(self) -> None: - """This is a temporary folder, is already empty to start with.""" - - def close(self) -> None: - LOGGER.debug("remove temporary app data folder %s", self.lock.path) - safe_delete(self.lock.path) - - def embed_update_log(self, distribution: str, for_py_version: str) -> NoReturn: - raise NotImplementedError - - -__all__ = [ - "TempAppData", -] diff --git a/src/virtualenv/config/cli/parser.py b/src/virtualenv/config/cli/parser.py deleted file mode 100644 index eeb58fc6b..000000000 --- a/src/virtualenv/config/cli/parser.py +++ /dev/null @@ -1,158 +0,0 @@ -from __future__ import annotations - -import os -from argparse import SUPPRESS, ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace -from collections import OrderedDict -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from argparse import Action - from collections.abc import Mapping, Sequence - -from virtualenv.config.convert import get_type -from virtualenv.config.env_var import get_env_var -from virtualenv.config.ini import IniConfig - - -class VirtualEnvOptions(Namespace): - def __init__(self, **kwargs: Any) -> None: # noqa: ANN401 - super().__init__(**kwargs) - self._src: str | None = None - self._sources: dict[str, str] = {} - - def set_src(self, key: str, value: Any, src: str) -> None: # noqa: ANN401 - """Set an option value and record where it came from. - - :param key: the option name - :param value: the option value - :param src: the source of the value (e.g. ``"cli"``, ``"env var"``, ``"default"``) - - """ - setattr(self, key, value) - if src.startswith("env var"): - src = "env var" - self._sources[key] = src - - def __setattr__(self, key: str, value: Any) -> None: # noqa: ANN401 - if (src := getattr(self, "_src", None)) is not None: - self._sources[key] = src - super().__setattr__(key, value) - - def get_source(self, key: str) -> str | None: - """Return the source that provided a given option value. - - :param key: the option name - - :returns: the source string (e.g. ``"cli"``, ``"env var"``, ``"default"``), or ``None`` if not tracked - - """ - return self._sources.get(key) - - @property - def verbosity(self) -> int | None: - """The verbosity level, computed as ``verbose - quiet``, clamped to zero. - - :returns: the verbosity level, or ``None`` if neither ``--verbose`` nor ``--quiet`` has been parsed yet - - """ - if not hasattr(self, "verbose") and not hasattr(self, "quiet"): - return None - return max(self.verbose - self.quiet, 0) - - def __repr__(self) -> str: - return f"{type(self).__name__}({', '.join(f'{k}={v}' for k, v in vars(self).items() if not k.startswith('_'))})" - - -class VirtualEnvConfigParser(ArgumentParser): - """Custom option parser which updates its defaults by checking the configuration files and environmental vars.""" - - def __init__( - self, - options: VirtualEnvOptions | None = None, - env: Mapping[str, str] | None = None, - *args: Any, # noqa: ANN401 - **kwargs: Any, # noqa: ANN401 - ) -> None: - env = os.environ if env is None else env - self.file_config = IniConfig(env) - self.epilog_list = [] - self.env = env - kwargs["epilog"] = self.file_config.epilog - kwargs["add_help"] = False - kwargs["formatter_class"] = HelpFormatter - kwargs["prog"] = "virtualenv" - super().__init__(*args, **kwargs) - self._fixed = set() - if options is not None and not isinstance(options, VirtualEnvOptions): - msg = "options must be of type VirtualEnvOptions" - raise TypeError(msg) - self.options = VirtualEnvOptions() if options is None else options - self._interpreter = None - self._app_data = None - - def _fix_defaults(self) -> None: - for action in self._actions: - action_id = id(action) - if action_id not in self._fixed: - self._fix_default(action) - self._fixed.add(action_id) - - def _fix_default(self, action: Action) -> None: - if hasattr(action, "default") and hasattr(action, "dest") and action.default != SUPPRESS: - as_type = get_type(action) - names = OrderedDict((i.lstrip("-").replace("-", "_"), None) for i in action.option_strings) - outcome = None - for name in names: - outcome = get_env_var(name, as_type, self.env) - if outcome is not None: - break - if outcome is None and self.file_config: - for name in names: - outcome = self.file_config.get(name, as_type) - if outcome is not None: - break - if outcome is not None: - action.default, action.default_source = outcome - else: - outcome = action.default, "default" - self.options.set_src(action.dest, *outcome) - - def enable_help(self) -> None: - self._fix_defaults() - self.add_argument("-h", "--help", action="help", default=SUPPRESS, help="show this help message and exit") - - def parse_known_args( - self, args: Sequence[str] | None = None, namespace: VirtualEnvOptions | None = None - ) -> tuple[VirtualEnvOptions, list[str]]: - if namespace is None: - namespace = self.options - elif namespace is not self.options: - msg = "can only pass in parser.options" - raise ValueError(msg) - self._fix_defaults() - self.options._src = "cli" # noqa: SLF001 - try: - namespace.env = self.env - return super().parse_known_args(args, namespace=namespace) - finally: - self.options._src = None # noqa: SLF001 - - -class HelpFormatter(ArgumentDefaultsHelpFormatter): - def __init__(self, prog: str, **kwargs: Any) -> None: # noqa: ANN401 - super().__init__(prog, max_help_position=32, width=240, **kwargs) - - def _get_help_string(self, action: Action) -> str | None: - text = super()._get_help_string(action) - if text is not None and hasattr(action, "default_source"): - default = " (default: %(default)s)" - if text.endswith(default): - text = f"{text[: -len(default)]} (default: %(default)s -> from %(default_source)s)" - return text - - -__all__ = [ - "HelpFormatter", - "VirtualEnvConfigParser", - "VirtualEnvOptions", -] diff --git a/src/virtualenv/config/convert.py b/src/virtualenv/config/convert.py deleted file mode 100644 index 8e5e5cac7..000000000 --- a/src/virtualenv/config/convert.py +++ /dev/null @@ -1,105 +0,0 @@ -from __future__ import annotations - -import logging -import os -from typing import TYPE_CHECKING, ClassVar - -if TYPE_CHECKING: - from argparse import Action - from typing import Any - -LOGGER = logging.getLogger(__name__) - - -class TypeData: - def __init__(self, default_type: type, as_type: type) -> None: - self.default_type = default_type - self.as_type = as_type - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(base={self.default_type}, as={self.as_type})" - - def convert(self, value: str) -> Any: # noqa: ANN401 - return self.default_type(value) - - -class BoolType(TypeData): - BOOLEAN_STATES: ClassVar[dict[str, bool]] = { - "1": True, - "yes": True, - "true": True, - "on": True, - "0": False, - "no": False, - "false": False, - "off": False, - } - - def convert(self, value: str) -> bool: - if value.lower() not in self.BOOLEAN_STATES: - msg = f"Not a boolean: {value}" - raise ValueError(msg) - return self.BOOLEAN_STATES[value.lower()] - - -class NoneType(TypeData): - def convert(self, value: str) -> str | None: - if not value: - return None - return str(value) - - -class ListType(TypeData): - def _validate(self) -> None: - """no op.""" - - def convert(self, value: str | list[str], flatten: bool = True) -> list[Any]: # noqa: ARG002, FBT002 - values = self.split_values(value) - result = [] - for a_value in values: - sub_values = a_value.split(os.pathsep) - result.extend(sub_values) - return [self.as_type(i) for i in result] - - def split_values(self, value: str | bytes | list[str]) -> list[str]: - """Split the provided value into a list. - - First this is done by newlines. If there were no newlines in the text, then we next try to split by comma. - - """ - if isinstance(value, (str, bytes)): - # Use `splitlines` rather than a custom check for whether there is - # more than one line. This ensures that the full `splitlines()` - # logic is supported here. - values = value.splitlines() - if len(values) <= 1: - values = value.split(",") # ty: ignore[invalid-argument-type] - values = filter(None, [x.strip() for x in values]) - else: - values = list(value) - - return values # ty: ignore[invalid-return-type] - - -def convert(value: str, as_type: TypeData, source: str) -> Any: # noqa: ANN401 - """Convert the value as a given type where the value comes from the given source.""" - try: - return as_type.convert(value) - except Exception as exception: - LOGGER.warning("%s failed to convert %r as %r because %r", source, value, as_type, exception) - raise - - -_CONVERT = {bool: BoolType, type(None): NoneType, list: ListType} - - -def get_type(action: Action) -> TypeData: - default_type = type(action.default) - as_type = default_type if action.type is None else action.type - return _CONVERT.get(default_type, TypeData)(default_type, as_type) # ty: ignore[invalid-argument-type] - - -__all__ = [ - "convert", - "get_type", -] diff --git a/src/virtualenv/config/env_var.py b/src/virtualenv/config/env_var.py deleted file mode 100644 index 498e3db9c..000000000 --- a/src/virtualenv/config/env_var.py +++ /dev/null @@ -1,38 +0,0 @@ -from __future__ import annotations - -from contextlib import suppress -from typing import TYPE_CHECKING - -from .convert import convert - -if TYPE_CHECKING: - from collections.abc import Mapping - from typing import Any - - from .convert import TypeData - - -def get_env_var(key: str, as_type: TypeData, env: Mapping[str, str]) -> tuple[Any, str] | None: - """Get the environment variable option. - - :param key: the config key requested - :param as_type: the type we would like to convert it to - :param env: environment variables to use - - :returns: the converted value and source, or None if not set - - """ - environ_key = f"VIRTUALENV_{key.upper()}" - if env.get(environ_key): - value = env[environ_key] - - with suppress(Exception): # note the converter already logs a warning when failures happen - source = f"env var {environ_key}" - as_type = convert(value, as_type, source) - return as_type, source - return None - - -__all__ = [ - "get_env_var", -] diff --git a/src/virtualenv/config/ini.py b/src/virtualenv/config/ini.py deleted file mode 100644 index 67695d3ee..000000000 --- a/src/virtualenv/config/ini.py +++ /dev/null @@ -1,83 +0,0 @@ -from __future__ import annotations - -import logging -import os -from configparser import ConfigParser -from pathlib import Path -from typing import TYPE_CHECKING, ClassVar - -from platformdirs import user_config_dir - -from .convert import convert - -if TYPE_CHECKING: - from collections.abc import Mapping - from typing import Any - - from .convert import TypeData - -LOGGER = logging.getLogger(__name__) - - -class IniConfig: - VIRTUALENV_CONFIG_FILE_ENV_VAR: ClassVar[str] = "VIRTUALENV_CONFIG_FILE" - STATE: ClassVar[dict[bool | None, str]] = {None: "failed to parse", True: "active", False: "missing"} - - section = "virtualenv" - - def __init__(self, env: Mapping[str, str] | None = None) -> None: - env = os.environ if env is None else env - config_file = env.get(self.VIRTUALENV_CONFIG_FILE_ENV_VAR, None) - self.is_env_var = config_file is not None - if config_file is None: - config_file = Path(user_config_dir(appname="virtualenv", appauthor="pypa")) / "virtualenv.ini" - else: - config_file = Path(config_file) - self.config_file = config_file - self._cache = {} - - exception = None - self.has_config_file = None - try: - self.has_config_file = self.config_file.exists() - except OSError as exc: - exception = exc - else: - if self.has_config_file: - self.config_file = self.config_file.resolve() - self.config_parser = ConfigParser() - try: - self._load() - self.has_virtualenv_section = self.config_parser.has_section(self.section) - except Exception as exc: # noqa: BLE001 - exception = exc - if exception is not None: - LOGGER.error("failed to read config file %s because %r", config_file, exception) - - def _load(self) -> None: - with self.config_file.open("rt", encoding="utf-8") as file_handler: - return self.config_parser.read_file(file_handler) - - def get(self, key: str, as_type: TypeData) -> tuple[Any, str] | None: - cache_key = key, as_type - if cache_key in self._cache: - return self._cache[cache_key] - try: - source = "file" - raw_value = self.config_parser.get(self.section, key.lower()) - value = convert(raw_value, as_type, source) - result = value, source - except Exception: # noqa: BLE001 - result = None - self._cache[cache_key] = result - return result - - def __bool__(self) -> bool: - return bool(self.has_config_file) and bool(self.has_virtualenv_section) - - @property - def epilog(self) -> str: - return ( - f"\nconfig file {self.config_file} {self.STATE[self.has_config_file]} " - f"(change{'d' if self.is_env_var else ''} via env var {self.VIRTUALENV_CONFIG_FILE_ENV_VAR})" - ) diff --git a/src/virtualenv/create/__init__.py b/src/virtualenv/create/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/virtualenv/create/creator.py b/src/virtualenv/create/creator.py deleted file mode 100644 index 965857491..000000000 --- a/src/virtualenv/create/creator.py +++ /dev/null @@ -1,291 +0,0 @@ -from __future__ import annotations - -import json -import logging -import os -import sys -import textwrap -from abc import ABC, abstractmethod -from argparse import ArgumentTypeError -from ast import literal_eval -from collections import OrderedDict -from pathlib import Path -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from argparse import ArgumentParser - from typing import Any, NoReturn - - from python_discovery import PythonInfo - - from virtualenv.app_data.base import AppData - from virtualenv.config.cli.parser import VirtualEnvOptions - -from os.path import commonpath - -from virtualenv.util.path import safe_delete -from virtualenv.util.subprocess import LogCmd, run_cmd -from virtualenv.version import __version__ - -from .pyenv_cfg import PyEnvCfg - -HERE = Path(os.path.abspath(__file__)).parent -DEBUG_SCRIPT = HERE / "debug.py" -LOGGER = logging.getLogger(__name__) - - -class CreatorMeta: - def __init__(self) -> None: - self.error = None - - -class Creator(ABC): - """A class that given a python Interpreter creates a virtual environment.""" - - def __init__(self, options: VirtualEnvOptions, interpreter: PythonInfo) -> None: - """Construct a new virtual environment creator. - - :param options: the CLI option as parsed from :meth:`add_parser_arguments` - :param interpreter: the interpreter to create virtual environment from - - """ - self.interpreter = interpreter - self._debug = None - self.dest = Path(options.dest) - self.clear = options.clear - self.no_vcs_ignore = options.no_vcs_ignore - self.pyenv_cfg = PyEnvCfg.from_folder(self.dest) - self.app_data = options.app_data - self.env = options.env - self.prompt = getattr(options, "prompt", None) - - if TYPE_CHECKING: - - @property - def exe(self) -> Path: ... - - @property - def env_name(self) -> str: ... - - @property - def bin_dir(self) -> Path: ... - - @property - def script_dir(self) -> Path: ... - - @property - def libs(self) -> list[Path]: ... - - @property - def purelib(self) -> Path: ... - - @property - def platlib(self) -> Path: ... - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in self._args())})" - - def _args(self) -> list[tuple[str, Any]]: - return [ - ("dest", str(self.dest)), - ("clear", self.clear), - ("no_vcs_ignore", self.no_vcs_ignore), - ] - - @classmethod - def can_create(cls, interpreter: PythonInfo) -> CreatorMeta | None: # noqa: ARG003 - """Determine if we can create a virtual environment. - - :param interpreter: the interpreter in question - - :returns: ``None`` if we can't create, any other object otherwise that will be forwarded to - :meth:`add_parser_arguments` - - """ - return True # type: ignore[return-value] - - @classmethod - def add_parser_arguments( - cls, - parser: ArgumentParser, - interpreter: PythonInfo, # noqa: ARG003 - meta: CreatorMeta, # noqa: ARG003 - app_data: AppData, # noqa: ARG003 - ) -> None: - """Add CLI arguments for the creator. - - :param parser: the CLI parser - :param app_data: the application data folder - :param interpreter: the interpreter we're asked to create virtual environment for - :param meta: value as returned by :meth:`can_create` - - """ - parser.add_argument( - "dest", - help="directory to create virtualenv at", - type=cls.validate_dest, - ) - parser.add_argument( - "--clear", - dest="clear", - action="store_true", - help="remove the destination directory if exist before starting (will overwrite files otherwise)", - default=False, - ) - parser.add_argument( - "--no-vcs-ignore", - dest="no_vcs_ignore", - action="store_true", - help="don't create VCS ignore directive in the destination directory", - default=False, - ) - - @abstractmethod - def create(self) -> None: - """Perform the virtual environment creation.""" - raise NotImplementedError - - @classmethod - def validate_dest(cls, raw_value: str) -> str: # noqa: C901 - """No path separator in the path, valid chars and must be write-able.""" - - def non_write_able(dest: Path, value: Path) -> NoReturn: - common = Path(commonpath([str(value), str(dest)])) - msg = f"the destination {dest.relative_to(common)} is not write-able at {common}" - raise ArgumentTypeError(msg) - - # the file system must be able to encode - # note in newer CPython this is always utf-8 https://www.python.org/dev/peps/pep-0529/ - encoding = sys.getfilesystemencoding() - refused = OrderedDict() - kwargs = {"errors": "ignore"} if encoding != "mbcs" else {} - for char in str(raw_value): - try: - trip = char.encode(encoding, **kwargs).decode(encoding) - if trip == char: - continue - raise ValueError(trip) # noqa: TRY301 - except ValueError: - refused[char] = None - if refused: - bad = "".join(refused.keys()) - msg = f"the file system codec ({encoding}) cannot handle characters {bad!r} within {raw_value!r}" - raise ArgumentTypeError(msg) - if os.pathsep in raw_value: - msg = ( - f"destination {raw_value!r} must not contain the path separator ({os.pathsep})" - f" as this would break the activation scripts" - ) - raise ArgumentTypeError(msg) - - value = Path(raw_value) - if value.exists() and value.is_file(): - msg = f"the destination {value} already exists and is a file" - raise ArgumentTypeError(msg) - dest = Path(os.path.abspath(str(value))).resolve() # on Windows absolute does not imply resolve so use both - value = dest - while dest: - if dest.exists(): - if os.access(str(dest), os.W_OK): - break - non_write_able(dest, value) - base, _ = dest.parent, dest.name - if base == dest: - non_write_able(dest, value) # pragma: no cover - dest = base - return str(value) - - def run(self) -> None: - if self.dest.exists() and self.clear: - LOGGER.debug("delete %s", self.dest) - safe_delete(self.dest) - self.create() - self.add_cachedir_tag() - self.set_pyenv_cfg() - if not self.no_vcs_ignore: - self.setup_ignore_vcs() - - def add_cachedir_tag(self) -> None: - """Generate a file indicating that this is not meant to be backed up.""" - cachedir_tag_file = self.dest / "CACHEDIR.TAG" - if not cachedir_tag_file.exists(): - cachedir_tag_text = textwrap.dedent(""" - Signature: 8a477f597d28d172789f06886806bc55 - # This file is a cache directory tag created by Python virtualenv. - # For information about cache directory tags, see: - # https://bford.info/cachedir/ - """).strip() - cachedir_tag_file.write_text(cachedir_tag_text, encoding="utf-8") - - def set_pyenv_cfg(self) -> None: - self.pyenv_cfg.content = OrderedDict() - system_executable = self.interpreter.system_executable or self.interpreter.executable - assert system_executable is not None # noqa: S101 - self.pyenv_cfg["home"] = os.path.dirname(os.path.abspath(system_executable)) - self.pyenv_cfg["implementation"] = self.interpreter.implementation - self.pyenv_cfg["version_info"] = ".".join(str(i) for i in self.interpreter.version_info) - self.pyenv_cfg["version"] = ".".join(str(i) for i in self.interpreter.version_info[:3]) - self.pyenv_cfg["executable"] = os.path.realpath(system_executable) - self.pyenv_cfg["command"] = f"{sys.executable} -m virtualenv {self.dest}" - self.pyenv_cfg["virtualenv"] = __version__ - if self.prompt is not None: - prompt_value = os.path.basename(os.getcwd()) if self.prompt == "." else self.prompt - self.pyenv_cfg["prompt"] = prompt_value - - def setup_ignore_vcs(self) -> None: - """Generate ignore instructions for version control systems.""" - # mark this folder to be ignored by VCS, handle https://www.python.org/dev/peps/pep-0610/#registered-vcs - git_ignore = self.dest / ".gitignore" - if not git_ignore.exists(): - git_ignore.write_text("# created by virtualenv automatically\n*\n", encoding="utf-8") - # Mercurial - does not support the .hgignore file inside a subdirectory directly, but only if included via the - # subinclude directive from root, at which point on might as well ignore the directory itself, see - # https://www.selenic.com/mercurial/hgignore.5.html for more details - # Bazaar - does not support ignore files in sub-directories, only at root level via .bzrignore - # Subversion - does not support ignore files, requires direct manipulation with the svn tool - - @property - def debug(self) -> dict[str, Any] | None: - """:returns: debug information about the virtual environment (only valid after :meth:`create` has run)""" - if self._debug is None and self.exe is not None: - self._debug = get_env_debug_info(self.exe, self.debug_script(), self.app_data, self.env) - return self._debug - - @staticmethod - def debug_script() -> Path: - return DEBUG_SCRIPT - - -def get_env_debug_info(env_exe: Path, debug_script: Path, app_data: AppData, env: dict[str, str]) -> dict[str, Any]: - env = env.copy() - env.pop("PYTHONPATH", None) - - with app_data.ensure_extracted(debug_script) as debug_script_extracted: - cmd = [str(env_exe), str(debug_script_extracted)] - LOGGER.debug("debug via %r", LogCmd(cmd)) - code, out, err = run_cmd(cmd) - - try: - if code != 0: - if out: - result = literal_eval(out) - else: - if code == 2 and "file" in err: # noqa: PLR2004 - # Re-raise FileNotFoundError from `run_cmd()` - raise OSError(err) # noqa: TRY301 - raise Exception(err) # noqa: TRY002, TRY301 - else: - result = json.loads(out) - if err: - result["err"] = err - except Exception as exception: # noqa: BLE001 - return {"out": out, "err": err, "returncode": code, "exception": repr(exception)} - if "sys" in result and "path" in result["sys"]: - del result["sys"]["path"][0] - return result - - -__all__ = [ - "Creator", - "CreatorMeta", -] diff --git a/src/virtualenv/create/debug.py b/src/virtualenv/create/debug.py deleted file mode 100644 index c56aab982..000000000 --- a/src/virtualenv/create/debug.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Inspect a target Python interpreter virtual environment wise.""" - -from __future__ import annotations - -import sys # built-in - - -def encode_path(value: object) -> str | None: - if value is None: - return None - if not isinstance(value, (str, bytes)): - value = repr(value) if isinstance(value, type) else repr(type(value)) - if isinstance(value, bytes): - value = value.decode(sys.getfilesystemencoding()) - return value - - -def encode_list_path(value: list[object]) -> list[str | None]: - return [encode_path(i) for i in value] - - -def run() -> None: # noqa: C901 - """Print debug data about the virtual environment.""" - try: - from collections import OrderedDict # noqa: PLC0415 - except ImportError: # pragma: no cover - # this is possible if the standard library cannot be accessed - - OrderedDict = dict # type: ignore[misc] # pragma: no cover # noqa: N806 - result: dict = OrderedDict([("sys", OrderedDict())]) - path_keys = ( - "executable", - "_base_executable", - "prefix", - "base_prefix", - "real_prefix", - "exec_prefix", - "base_exec_prefix", - "path", - "meta_path", - ) - for key in path_keys: - value = getattr(sys, key, None) - value = encode_list_path(value) if isinstance(value, list) else encode_path(value) - result["sys"][key] = value - result["sys"]["fs_encoding"] = sys.getfilesystemencoding() - result["sys"]["io_encoding"] = getattr(sys.stdout, "encoding", None) - result["version"] = sys.version - - try: - import sysconfig # noqa: PLC0415 - - # https://bugs.python.org/issue22199 - makefile = getattr(sysconfig, "get_makefile_filename", getattr(sysconfig, "_get_makefile_filename", None)) - if makefile is not None: - result["makefile_filename"] = encode_path(makefile()) - except ImportError: - pass - - import os # landmark # noqa: PLC0415 - - result["os"] = repr(os) - - try: - import site # site # noqa: PLC0415 - - result["site"] = repr(site) - except ImportError as exception: # pragma: no cover - result["site"] = repr(exception) # pragma: no cover - - try: - import datetime # site # noqa: PLC0415 - - result["datetime"] = repr(datetime) - except ImportError as exception: # pragma: no cover - result["datetime"] = repr(exception) # pragma: no cover - - try: - import math # site # noqa: PLC0415 - - result["math"] = repr(math) - except ImportError as exception: # pragma: no cover - result["math"] = repr(exception) # pragma: no cover - - # try to print out, this will validate if other core modules are available (json in this case) - try: - import json # noqa: PLC0415 - - result["json"] = repr(json) - except ImportError as exception: - result["json"] = repr(exception) - else: - try: - content = json.dumps(result, indent=2) - sys.stdout.write(content) - except (ValueError, TypeError) as exception: # pragma: no cover - sys.stderr.write(repr(exception)) - sys.stdout.write(repr(result)) # pragma: no cover - raise SystemExit(1) # noqa: B904 # pragma: no cover - - -if __name__ == "__main__": - run() diff --git a/src/virtualenv/create/describe.py b/src/virtualenv/create/describe.py deleted file mode 100644 index a10ffd17b..000000000 --- a/src/virtualenv/create/describe.py +++ /dev/null @@ -1,116 +0,0 @@ -from __future__ import annotations - -from abc import ABC -from collections import OrderedDict -from pathlib import Path -from typing import TYPE_CHECKING - -from virtualenv.info import IS_WIN - -if TYPE_CHECKING: - from typing import Any - - from python_discovery import PythonInfo - - -class Describe: - """Given a host interpreter tell us information about what the created interpreter might look like.""" - - suffix = ".exe" if IS_WIN else "" - - def __init__(self, dest: Path, interpreter: PythonInfo) -> None: - self.interpreter = interpreter - self.dest = dest - self._stdlib = None - self._stdlib_platform = None - self._system_stdlib = None - self._conf_vars = None - - @property - def bin_dir(self) -> Path: - return self.script_dir - - @property - def script_dir(self) -> Path: - return self.dest / self.interpreter.install_path("scripts") - - @property - def purelib(self) -> Path: - return self.dest / self.interpreter.install_path("purelib") - - @property - def platlib(self) -> Path: - return self.dest / self.interpreter.install_path("platlib") - - @property - def libs(self) -> list[Path]: - return list(OrderedDict(((self.platlib, None), (self.purelib, None))).keys()) - - @property - def stdlib(self) -> Path: - if self._stdlib is None: - self._stdlib = Path(self.interpreter.sysconfig_path("stdlib", config_var=self._config_vars)) - return self._stdlib - - @property - def stdlib_platform(self) -> Path: - if self._stdlib_platform is None: - self._stdlib_platform = Path(self.interpreter.sysconfig_path("platstdlib", config_var=self._config_vars)) - return self._stdlib_platform - - @property - def _config_vars(self) -> dict[str, Any]: - if self._conf_vars is None: - self._conf_vars = self._calc_config_vars(self.dest) - return self._conf_vars - - def _calc_config_vars(self, to: Path) -> dict[str, Any]: - sys_vars = self.interpreter.sysconfig_vars - return { - k: (to if isinstance(v, str) and v.startswith(self.interpreter.prefix) else v) for k, v in sys_vars.items() - } - - @classmethod - def can_describe(cls, interpreter: PythonInfo) -> bool: # noqa: ARG003 - """Knows means it knows how the output will look.""" - return True - - @property - def env_name(self) -> str: - return self.dest.parts[-1] - - @property - def exe(self) -> Path: - return self.bin_dir / f"{self.exe_stem()}{self.suffix}" - - @classmethod - def exe_stem(cls) -> str: - """Executable name without suffix - there seems to be no standard way to get this without creating it.""" - raise NotImplementedError - - def script(self, name: str) -> Path: - return self.script_dir / f"{name}{self.suffix}" - - -class Python3Supports(Describe, ABC): - pass - - -class PosixSupports(Describe, ABC): - @classmethod - def can_describe(cls, interpreter: PythonInfo) -> bool: - return interpreter.os == "posix" and super().can_describe(interpreter) - - -class WindowsSupports(Describe, ABC): - @classmethod - def can_describe(cls, interpreter: PythonInfo) -> bool: - return interpreter.os == "nt" and super().can_describe(interpreter) - - -__all__ = [ - "Describe", - "PosixSupports", - "Python3Supports", - "WindowsSupports", -] diff --git a/src/virtualenv/create/pyenv_cfg.py b/src/virtualenv/create/pyenv_cfg.py deleted file mode 100644 index 6901850fd..000000000 --- a/src/virtualenv/create/pyenv_cfg.py +++ /dev/null @@ -1,79 +0,0 @@ -from __future__ import annotations - -import logging -import os -from collections import OrderedDict -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from pathlib import Path - -LOGGER = logging.getLogger(__name__) - - -class PyEnvCfg: - def __init__(self, content: OrderedDict[str, str], path: Path) -> None: - self.content = content - self.path = path - - @classmethod - def from_folder(cls, folder: Path) -> PyEnvCfg: - return cls.from_file(folder / "pyvenv.cfg") - - @classmethod - def from_file(cls, path: Path) -> PyEnvCfg: - content = cls._read_values(path) if path.exists() else OrderedDict() - return PyEnvCfg(content, path) - - @staticmethod - def _read_values(path: Path) -> OrderedDict[str, str]: - content = OrderedDict() - for line in path.read_text(encoding="utf-8").splitlines(): - equals_at = line.index("=") - key = line[:equals_at].strip() - value = line[equals_at + 1 :].strip() - if len(value) > 1 and value[0] in {"'", '"'} and value[0] == value[-1]: - value = value[1:-1] - content[key] = value - return content - - def write(self) -> None: - LOGGER.debug("write %s", self.path) - text = "" - for key, value in self.content.items(): - # Use abspath to normalize relative paths but preserve symlinks (match venv behavior) - # See issue #2770 - realpath resolves symlinks which breaks prefix symlinks - if key == "prompt" and value: - normalized_value = f'"{value}"' - else: - normalized_value = os.path.abspath(value) if value and os.path.exists(value) else value - line = f"{key} = {normalized_value}" - LOGGER.debug("\t%s", line) - text += line - text += "\n" - self.path.write_text(text, encoding="utf-8") - - def refresh(self) -> OrderedDict[str, str]: - self.content = self._read_values(self.path) - return self.content - - def __setitem__(self, key: str, value: str) -> None: - self.content[key] = value - - def __getitem__(self, key: str) -> str: - return self.content[key] - - def __contains__(self, item: str) -> bool: - return item in self.content - - def update(self, other: dict[str, str]) -> PyEnvCfg: - self.content.update(other) - return self - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(path={self.path})" - - -__all__ = [ - "PyEnvCfg", -] diff --git a/src/virtualenv/create/via_global_ref/__init__.py b/src/virtualenv/create/via_global_ref/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/virtualenv/create/via_global_ref/_virtualenv.py b/src/virtualenv/create/via_global_ref/_virtualenv.py deleted file mode 100644 index f63332ce5..000000000 --- a/src/virtualenv/create/via_global_ref/_virtualenv.py +++ /dev/null @@ -1,128 +0,0 @@ -"""Patches that are applied at runtime to the virtual environment.""" - -from __future__ import annotations - -import contextlib -import os -import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - import types - from collections.abc import Callable - from importlib.machinery import ModuleSpec - -VIRTUALENV_PATCH_FILE = os.path.abspath(__file__) - - -def patch_dist(dist: types.ModuleType) -> None: - """Distutils allows user to configure some arguments via a configuration file: https://docs.python.org/3/install/index.html#distutils-configuration-files. - - Some of this arguments though don't make sense in context of the virtual environment files, let's fix them up. - - """ - # we cannot allow some install config as that would get packages installed outside of the virtual environment - old_parse_config_files = dist.Distribution.parse_config_files - - def parse_config_files(self, *args: object, **kwargs: object) -> object: # noqa: ANN001 - result = old_parse_config_files(self, *args, **kwargs) - install = self.get_option_dict("install") - - if "prefix" in install: # the prefix governs where to install the libraries - install["prefix"] = VIRTUALENV_PATCH_FILE, os.path.abspath(sys.prefix) - for base in ("purelib", "platlib", "headers", "scripts", "data"): - key = f"install_{base}" - if key in install: # do not allow global configs to hijack venv paths - install.pop(key, None) - return result - - dist.Distribution.parse_config_files = parse_config_files - - -# Import hook that patches some modules to ignore configuration values that break package installation in case -# of virtual environments. -_DISTUTILS_PATCH = "distutils.dist", "setuptools.dist" -# https://docs.python.org/3/library/importlib.html#setting-up-an-importer - - -class _Finder: - """A meta path finder that allows patching the imported distutils modules.""" - - fullname = None - - # lock[0] is threading.Lock(), but initialized lazily to avoid importing threading very early at startup, - # because there are gevent-based applications that need to be first to import threading by themselves. - # See https://github.com/pypa/virtualenv/issues/1895 for details. - lock = [] # noqa: RUF012 - - def find_spec(self, fullname: str, path: object, target: object = None) -> ModuleSpec | None: # noqa: ARG002 - # Guard against race conditions during file rewrite by checking if _DISTUTILS_PATCH is defined. - # This can happen when the file is being overwritten while it's being imported by another process. - # See https://github.com/pypa/virtualenv/issues/2969 for details. - try: - distutils_patch = _DISTUTILS_PATCH - except NameError: - return None - if fullname in distutils_patch and self.fullname is None: # noqa: PLR1702 - # initialize lock[0] lazily - if len(self.lock) == 0: - import threading # noqa: PLC0415 - - lock = threading.Lock() - # there is possibility that two threads T1 and T2 are simultaneously running into find_spec, - # observing .lock as empty, and further going into hereby initialization. However due to the GIL, - # list.append() operation is atomic and this way only one of the threads will "win" to put the lock - # - that every thread will use - into .lock[0]. - # https://docs.python.org/3/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe - self.lock.append(lock) - - from functools import partial # noqa: PLC0415 - from importlib.util import find_spec # noqa: PLC0415 - - with self.lock[0]: - self.fullname = fullname - try: - spec = find_spec(fullname, path) # ty: ignore[invalid-argument-type] - if spec is not None: - # https://www.python.org/dev/peps/pep-0451/#how-loading-will-work - is_new_api = hasattr(spec.loader, "exec_module") - func_name = "exec_module" if is_new_api else "load_module" - old = getattr(spec.loader, func_name) - func = self.exec_module if is_new_api else self.load_module - if old is not func: - try: # noqa: SIM105 - setattr(spec.loader, func_name, partial(func, old)) - except AttributeError: - pass # C-Extension loaders are r/o such as zipimporter with <3.7 - return spec - finally: - self.fullname = None - return None - - @staticmethod - def exec_module(old: Callable[..., object], module: types.ModuleType) -> None: - old(module) - try: - distutils_patch = _DISTUTILS_PATCH - except NameError: - return - if module.__name__ in distutils_patch: - # patch_dist or its dependencies may not be defined during file rewrite - with contextlib.suppress(NameError): - patch_dist(module) - - @staticmethod - def load_module(old: Callable[..., types.ModuleType], name: str) -> types.ModuleType: - module = old(name) - try: - distutils_patch = _DISTUTILS_PATCH - except NameError: - return module - if module.__name__ in distutils_patch: - # patch_dist or its dependencies may not be defined during file rewrite - with contextlib.suppress(NameError): - patch_dist(module) - return module - - -sys.meta_path.insert(0, _Finder()) diff --git a/src/virtualenv/create/via_global_ref/api.py b/src/virtualenv/create/via_global_ref/api.py deleted file mode 100644 index 5cb8e1297..000000000 --- a/src/virtualenv/create/via_global_ref/api.py +++ /dev/null @@ -1,142 +0,0 @@ -from __future__ import annotations - -import logging -import os -from abc import ABC -from pathlib import Path -from typing import TYPE_CHECKING - -from virtualenv.create.creator import Creator, CreatorMeta -from virtualenv.info import fs_supports_symlink - -if TYPE_CHECKING: - from argparse import ArgumentParser - from typing import Any - - from python_discovery import PythonInfo - - from virtualenv.app_data.base import AppData - from virtualenv.config.cli.parser import VirtualEnvOptions - -LOGGER = logging.getLogger(__name__) - - -class ViaGlobalRefMeta(CreatorMeta): - def __init__(self) -> None: - super().__init__() - self.copy_error = None - self.symlink_error = None - if not fs_supports_symlink(): - self.symlink_error = "the filesystem does not supports symlink" - - @property - def can_copy(self) -> bool: - return not self.copy_error - - @property - def can_symlink(self) -> bool: - return not self.symlink_error - - -class ViaGlobalRefApi(Creator, ABC): - def __init__(self, options: VirtualEnvOptions, interpreter: PythonInfo) -> None: - super().__init__(options, interpreter) - self.symlinks = self._should_symlink(options) - self.enable_system_site_package = options.system_site - - if TYPE_CHECKING: - - @property - def purelib(self) -> Path: ... - - @property - def script_dir(self) -> Path: ... - - @staticmethod - def _should_symlink(options: VirtualEnvOptions) -> bool: - # Priority of where the option is set to follow the order: CLI, env var, file, hardcoded. - # If both set at same level prefers copy over symlink. - copies, symlinks = getattr(options, "copies", False), getattr(options, "symlinks", False) - copy_src, sym_src = options.get_source("copies"), options.get_source("symlinks") - for level in ["cli", "env var", "file", "default"]: - s_opt = symlinks if sym_src == level else None - c_opt = copies if copy_src == level else None - if s_opt is True and c_opt is True: - return False - if s_opt is True: - return True - if c_opt is True: - return False - return False # fallback to copy - - @classmethod - def add_parser_arguments( - cls, parser: ArgumentParser, interpreter: PythonInfo, meta: ViaGlobalRefMeta, app_data: AppData - ) -> None: # ty: ignore[invalid-method-override] - super().add_parser_arguments(parser, interpreter, meta, app_data) - parser.add_argument( - "--system-site-packages", - default=False, - action="store_true", - dest="system_site", - help="give the virtual environment access to the system site-packages dir", - ) - if not meta.can_symlink and not meta.can_copy: - errors = [] - if meta.symlink_error: - errors.append(f"symlink: {meta.symlink_error}") - if meta.copy_error: - errors.append(f"copy: {meta.copy_error}") - msg = f"neither symlink or copy method supported: {', '.join(errors)}" - raise RuntimeError(msg) - group = parser.add_mutually_exclusive_group() - if meta.can_symlink: - group.add_argument( - "--symlinks", - default=True, - action="store_true", - dest="symlinks", - help="try to use symlinks rather than copies, when symlinks are not the default for the platform", - ) - if meta.can_copy: - group.add_argument( - "--copies", - "--always-copy", - default=not meta.can_symlink, - action="store_true", - dest="copies", - help="try to use copies rather than symlinks, even when symlinks are the default for the platform", - ) - - def create(self) -> None: - self.install_patch() - - def install_patch(self) -> None: - text = self.env_patch_text() - if text: - pth = self.purelib / "_virtualenv.pth" - LOGGER.debug("create virtualenv import hook file %s", pth) - pth.write_text("import _virtualenv", encoding="utf-8") - dest_path = self.purelib / "_virtualenv.py" - LOGGER.debug("create %s", dest_path) - dest_path.write_text(text, encoding="utf-8") - - def env_patch_text(self) -> str: - """Patch the distutils package to not be derailed by its configuration files.""" - with self.app_data.ensure_extracted(Path(__file__).parent / "_virtualenv.py") as resolved_path: - text = resolved_path.read_text(encoding="utf-8") - # script_dir and purelib are defined in subclasses - return text.replace('"__SCRIPT_DIR__"', repr(os.path.relpath(str(self.script_dir), str(self.purelib)))) - - def _args(self) -> list[tuple[str, Any]]: - return [*super()._args(), ("global", self.enable_system_site_package)] - - def set_pyenv_cfg(self) -> None: - super().set_pyenv_cfg() - self.pyenv_cfg["include-system-site-packages"] = "true" if self.enable_system_site_package else "false" - - -__all__ = [ - "ViaGlobalRefApi", - "ViaGlobalRefMeta", -] diff --git a/src/virtualenv/create/via_global_ref/builtin/__init__.py b/src/virtualenv/create/via_global_ref/builtin/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/virtualenv/create/via_global_ref/builtin/builtin_way.py b/src/virtualenv/create/via_global_ref/builtin/builtin_way.py deleted file mode 100644 index 005c23640..000000000 --- a/src/virtualenv/create/via_global_ref/builtin/builtin_way.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import annotations - -from abc import ABC -from typing import TYPE_CHECKING - -from virtualenv.create.creator import Creator -from virtualenv.create.describe import Describe - -if TYPE_CHECKING: - from python_discovery import PythonInfo - - from virtualenv.config.cli.parser import VirtualEnvOptions - - -class VirtualenvBuiltin(Creator, Describe, ABC): - """A creator that does operations itself without delegation, if we can create it we can also describe it.""" - - def __init__(self, options: VirtualEnvOptions, interpreter: PythonInfo) -> None: - Creator.__init__(self, options, interpreter) - Describe.__init__(self, self.dest, interpreter) - - -__all__ = [ - "VirtualenvBuiltin", -] diff --git a/src/virtualenv/create/via_global_ref/builtin/cpython/__init__.py b/src/virtualenv/create/via_global_ref/builtin/cpython/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/virtualenv/create/via_global_ref/builtin/cpython/common.py b/src/virtualenv/create/via_global_ref/builtin/cpython/common.py deleted file mode 100644 index 7fba31c3e..000000000 --- a/src/virtualenv/create/via_global_ref/builtin/cpython/common.py +++ /dev/null @@ -1,99 +0,0 @@ -from __future__ import annotations - -import re -from abc import ABC -from collections import OrderedDict -from pathlib import Path -from typing import TYPE_CHECKING - -from virtualenv.create.describe import PosixSupports, WindowsSupports -from virtualenv.create.via_global_ref.builtin.ref import RefMust, RefWhen -from virtualenv.create.via_global_ref.builtin.via_global_self_do import ViaGlobalRefVirtualenvBuiltin - -if TYPE_CHECKING: - from collections.abc import Generator - - from python_discovery import PythonInfo - - -class CPython(ViaGlobalRefVirtualenvBuiltin, ABC): - @classmethod - def can_describe(cls, interpreter: PythonInfo) -> bool: - return interpreter.implementation == "CPython" and super().can_describe(interpreter) - - @classmethod - def exe_stem(cls) -> str: - return "python" - - -class CPythonPosix(CPython, PosixSupports, ABC): - """Create a CPython virtual environment on POSIX platforms.""" - - @classmethod - def _executables(cls, interpreter: PythonInfo) -> Generator[tuple[Path, list[str], str, str]]: - host_exe = Path(interpreter.system_executable) # ty: ignore[invalid-argument-type] - minor = interpreter.version_info.minor - names = [ - "python", - "python3", - f"python3.{minor}", - *((f"python3.{minor}t",) if interpreter.free_threaded else ()), - host_exe.name, - ] - targets = OrderedDict((i, None) for i in names) - yield host_exe, list(targets.keys()), RefMust.NA, RefWhen.ANY - - -class CPythonWindows(CPython, WindowsSupports, ABC): - @classmethod - def _executables(cls, interpreter: PythonInfo) -> Generator[tuple[Path, list[str], str, str]]: - # symlink of the python executables does not work reliably, copy always instead - # - https://bugs.python.org/issue42013 - # - venv - host = cls.host_python(interpreter) - minor = interpreter.version_info.minor - names = { - "python.exe", - "python3.exe", - "python3", - host.name, - *((f"python3.{minor}t.exe",) if interpreter.free_threaded else ()), - } - for path in (host.parent / n for n in names): - yield host, [path.name], RefMust.COPY, RefWhen.ANY - # for more info on pythonw.exe see https://stackoverflow.com/a/30313091 - python_w = host.parent / "pythonw.exe" - yield ( - python_w, - [python_w.name, "pythonw3.exe", *((f"pythonw3.{minor}t.exe",) if interpreter.free_threaded else ())], - RefMust.COPY, - RefWhen.ANY, - ) - - @classmethod - def host_python(cls, interpreter: PythonInfo) -> Path: - return Path(interpreter.system_executable) # ty: ignore[invalid-argument-type] - - -def is_mac_os_framework(interpreter: PythonInfo) -> bool: - if interpreter.platform == "darwin": - return interpreter.sysconfig_vars.get("PYTHONFRAMEWORK") == "Python3" - return False - - -def is_macos_brew(interpreter: PythonInfo) -> bool: - return interpreter.platform == "darwin" and _BREW.fullmatch(interpreter.system_prefix) is not None - - -_BREW = re.compile( - 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/" - r"Python\.framework/Versions/3\.\d{1,2}", -) - -__all__ = [ - "CPython", - "CPythonPosix", - "CPythonWindows", - "is_mac_os_framework", - "is_macos_brew", -] diff --git a/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py b/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py deleted file mode 100644 index f66f28429..000000000 --- a/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py +++ /dev/null @@ -1,220 +0,0 @@ -from __future__ import annotations - -import abc -import fnmatch -from itertools import chain -from operator import methodcaller as method -from pathlib import Path -from textwrap import dedent -from typing import TYPE_CHECKING - -from virtualenv.create.describe import Python3Supports -from virtualenv.create.via_global_ref.builtin.ref import ExePathRefToDest, PathRefToDest, RefWhen -from virtualenv.create.via_global_ref.store import is_store_python -from virtualenv.util.path import copy as copy_path -from virtualenv.util.path import ensure_dir - -from .common import CPython, CPythonPosix, CPythonWindows, is_mac_os_framework, is_macos_brew - -if TYPE_CHECKING: - from collections.abc import Generator - - from python_discovery import PythonInfo - - from virtualenv.create.via_global_ref.builtin.ref import PathRef - from virtualenv.create.via_global_ref.builtin.via_global_self_do import BuiltinViaGlobalRefMeta - from virtualenv.create.via_global_ref.venv import Venv - - -class CPython3(CPython, Python3Supports, abc.ABC): - """CPython 3 or later.""" - - -class CPython3Posix(CPythonPosix, CPython3): - @classmethod - def can_describe(cls, interpreter: PythonInfo) -> bool: - return ( - is_mac_os_framework(interpreter) is False - and is_macos_brew(interpreter) is False - and super().can_describe(interpreter) - ) - - @classmethod - def sources(cls, interpreter: PythonInfo) -> Generator[PathRef]: # ty: ignore[invalid-method-override] - yield from super().sources(interpreter) - if shared_lib := cls._shared_libpython(interpreter): - yield PathRefToDest(shared_lib, dest=cls._to_lib, when=RefWhen.COPY) - - @classmethod - def _to_lib(cls, creator: CPython3Posix, src: Path) -> Path: - return creator.dest / "lib" / src.name - - @classmethod - def _shared_libpython(cls, interpreter: PythonInfo) -> Path | None: - if not interpreter.sysconfig_vars.get("Py_ENABLE_SHARED"): - return None - if not (instsoname := interpreter.sysconfig_vars.get("INSTSONAME")): - return None - if not (libdir := interpreter.sysconfig_vars.get("LIBDIR")): - return None - if not (lib_path := Path(libdir) / instsoname).exists(): - return None - return lib_path - - def install_venv_shared_libs(self, venv_creator: Venv) -> None: - if venv_creator.symlinks: - return - if not (shared_lib := self._shared_libpython(venv_creator.interpreter)): - return - dest = venv_creator.dest / "lib" / shared_lib.name - ensure_dir(dest.parent) - copy_path(shared_lib, dest) - - def env_patch_text(self) -> str: - text = super().env_patch_text() - if self.pyvenv_launch_patch_active(self.interpreter): - text += dedent( - """ - # for https://github.com/python/cpython/pull/9516, see https://github.com/pypa/virtualenv/issues/1704 - import os - if "__PYVENV_LAUNCHER__" in os.environ: - del os.environ["__PYVENV_LAUNCHER__"] - """, - ) - return text - - @classmethod - def pyvenv_launch_patch_active(cls, interpreter: PythonInfo) -> bool: - ver = interpreter.version_info - return interpreter.platform == "darwin" and ((3, 7, 8) > ver >= (3, 7) or (3, 8, 3) > ver >= (3, 8)) - - -class CPython3Windows(CPythonWindows, CPython3): - """CPython 3 on Windows.""" - - @classmethod - def setup_meta(cls, interpreter: PythonInfo) -> BuiltinViaGlobalRefMeta | None: # ty: ignore[invalid-method-override] - if is_store_python(interpreter): # store python is not supported here - return None - return super().setup_meta(interpreter) - - @classmethod - def sources(cls, interpreter: PythonInfo) -> Generator[PathRef]: # ty: ignore[invalid-method-override] - if cls.has_shim(interpreter): - refs = cls.executables(interpreter) - else: - refs = chain( - cls.executables(interpreter), # ty: ignore[invalid-argument-type] - cls.dll_and_pyd(interpreter), - cls.python_zip(interpreter), - ) - yield from refs - - @classmethod - def executables(cls, interpreter: PythonInfo) -> list[PathRef] | Generator[PathRef]: - sources = super().sources(interpreter) - if interpreter.version_info >= (3, 13): - t_suffix = "t" if interpreter.free_threaded else "" - updated_sources = [] - for ref in sources: - if ref.src.name == "python.exe": - launcher_path = ref.src.with_name(f"venvlauncher{t_suffix}.exe") - if launcher_path.exists(): - new_ref = ExePathRefToDest( - launcher_path, dest=ref.dest, targets=[ref.base, *ref.aliases], must=ref.must, when=ref.when - ) - updated_sources.append(new_ref) - continue - elif ref.src.name == "pythonw.exe": - w_launcher_path = ref.src.with_name(f"venvwlauncher{t_suffix}.exe") - if w_launcher_path.exists(): - new_ref = ExePathRefToDest( - w_launcher_path, - dest=ref.dest, - targets=[ref.base, *ref.aliases], - must=ref.must, - when=ref.when, - ) - updated_sources.append(new_ref) - continue - updated_sources.append(ref) - return updated_sources - return sources - - @classmethod - def has_shim(cls, interpreter: PythonInfo) -> bool: - return interpreter.version_info.minor >= 7 and cls.shim(interpreter) is not None # noqa: PLR2004 - - @classmethod - def shim(cls, interpreter: PythonInfo) -> Path | None: - root = Path(interpreter.system_stdlib) / "venv" / "scripts" / "nt" - if interpreter.version_info >= (3, 13): - # https://github.com/python/cpython/issues/112984 - t_suffix = "t" if interpreter.free_threaded else "" - exe_name = f"venvlauncher{t_suffix}.exe" - else: - exe_name = "python.exe" - if (shim := root / exe_name).exists(): - return shim - return None - - @classmethod - def host_python(cls, interpreter: PythonInfo) -> Path: - if cls.has_shim(interpreter): - # starting with CPython 3.7 Windows ships with a venvlauncher.exe that avoids the need for dll/pyd copies - # it also means the wrapper must be copied to avoid bugs such as https://bugs.python.org/issue42013 - return cls.shim(interpreter) # ty: ignore[invalid-return-type] - return super().host_python(interpreter) - - @classmethod - def dll_and_pyd(cls, interpreter: PythonInfo) -> Generator[PathRefToDest]: - folders = [Path(interpreter.system_executable).parent] # ty: ignore[invalid-argument-type] - - # May be missing on some Python hosts. - # See https://github.com/pypa/virtualenv/issues/2368 - dll_folder = Path(interpreter.system_prefix) / "DLLs" - if dll_folder.is_dir(): - folders.append(dll_folder) - - for folder in folders: - for file in folder.iterdir(): - if file.suffix in {".pyd", ".dll"}: - # Skip pywin32 DLLs to avoid conflicts with pywin32 installation - # pywin32 has its own post-install that places DLLs in site-packages/pywin32_system32 - # See https://github.com/pypa/virtualenv/issues/2662 - if cls._is_pywin32_dll(file.name): - continue - yield PathRefToDest(file, cls.to_bin) - - @classmethod - def _is_pywin32_dll(cls, filename: str) -> bool: - """Check if a DLL file belongs to pywin32.""" - # pywin32 DLLs follow patterns like: pywintypes39.dll, pythoncom39.dll - name_lower = filename.lower() - return name_lower.startswith(("pywintypes", "pythoncom")) - - @classmethod - def python_zip(cls, interpreter: PythonInfo) -> Generator[PathRefToDest]: - """``python{VERSION}.zip`` contains compiled ``*.pyc`` std lib packages, where ``VERSION`` is ``py_version_nodot`` var from the ``sysconfig`` module. - - See https://docs.python.org/3/using/windows.html#the-embeddable-package, ``discovery.py_info.PythonInfo`` class - (interpreter), and ``python -m sysconfig`` output. - - The embeddable Python distribution for Windows includes ``python{VERSION}.zip`` and ``python{VERSION}._pth`` - files. User can move/rename the zip file and edit ``sys.path`` by editing the ``_pth`` file. Here the - ``pattern`` is used only for the default zip file name. - - """ - pattern = f"*python{interpreter.version_nodot}.zip" - matches = fnmatch.filter(interpreter.path, pattern) - matched_paths = map(Path, matches) - existing_paths = filter(method("exists"), matched_paths) - if (path := next(existing_paths, None)) is not None: - yield PathRefToDest(path, cls.to_bin) - - -__all__ = [ - "CPython3", - "CPython3Posix", - "CPython3Windows", -] diff --git a/src/virtualenv/create/via_global_ref/builtin/cpython/mac_os.py b/src/virtualenv/create/via_global_ref/builtin/cpython/mac_os.py deleted file mode 100644 index 19d9a782b..000000000 --- a/src/virtualenv/create/via_global_ref/builtin/cpython/mac_os.py +++ /dev/null @@ -1,290 +0,0 @@ -"""The Apple Framework builds require their own customization.""" - -from __future__ import annotations - -import logging -import os -import struct -import subprocess -from abc import ABC, abstractmethod -from pathlib import Path -from textwrap import dedent -from typing import TYPE_CHECKING - -from virtualenv.create.via_global_ref.builtin.ref import ( - ExePathRefToDest, - PathRefToDest, - RefMust, -) -from virtualenv.create.via_global_ref.builtin.via_global_self_do import BuiltinViaGlobalRefMeta - -from .common import CPython, CPythonPosix, is_mac_os_framework, is_macos_brew -from .cpython3 import CPython3 - -if TYPE_CHECKING: - from collections.abc import Callable, Generator - from io import BufferedRandom - - from python_discovery import PythonInfo - - from virtualenv.create.via_global_ref.builtin.ref import PathRef - -LOGGER = logging.getLogger(__name__) - - -class CPythonmacOsFramework(CPython, ABC): - @classmethod - def can_describe(cls, interpreter: PythonInfo) -> bool: - return is_mac_os_framework(interpreter) and super().can_describe(interpreter) - - def create(self) -> None: - super().create() - - target = self.desired_mach_o_image_path() - current = self.current_mach_o_image_path() - for src in self._sources: - if isinstance(src, ExePathRefToDest) and (src.must == RefMust.COPY or not self.symlinks): - exes = [self.bin_dir / src.base] - if not self.symlinks: - exes.extend(self.bin_dir / a for a in src.aliases) - for exe in exes: - fix_mach_o(str(exe), current, target, self.interpreter.max_size) # ty: ignore[invalid-argument-type] - try: - subprocess.check_call(["codesign", "--force", "--sign", "-", str(exe)]) # noqa: S607 - except (OSError, subprocess.CalledProcessError) as e: - LOGGER.warning("Could not ad-hoc re-sign %s: %s", exe, e) - - @classmethod - def _executables(cls, interpreter: PythonInfo) -> Generator[tuple[Path, list[str], str, str]]: - for _, targets, must, when in super()._executables(interpreter): - # Make sure we use the embedded interpreter inside the framework, even if sys.executable points to the - # stub executable in ${sys.prefix}/bin. - # See http://groups.google.com/group/python-virtualenv/browse_thread/thread/17cab2f85da75951 - fixed_host_exe = Path(interpreter.prefix) / "Resources" / "Python.app" / "Contents" / "MacOS" / "Python" # ty: ignore[invalid-argument-type] - yield fixed_host_exe, targets, must, when - - @abstractmethod - def current_mach_o_image_path(self) -> str: - raise NotImplementedError - - @abstractmethod - def desired_mach_o_image_path(self) -> str: - raise NotImplementedError - - -class CPython3macOsFramework(CPythonmacOsFramework, CPython3, CPythonPosix): - def current_mach_o_image_path(self) -> str: - return "@executable_path/../../../../Python3" - - def desired_mach_o_image_path(self) -> str: - return "@executable_path/../.Python" - - @classmethod - def sources(cls, interpreter: PythonInfo) -> Generator[PathRef]: # ty: ignore[invalid-method-override] - yield from super().sources(interpreter) - - # add a symlink to the host python image - exe = Path(interpreter.prefix) / "Python3" # ty: ignore[invalid-argument-type] - yield PathRefToDest(exe, dest=lambda self, _: self.dest / ".Python", must=RefMust.SYMLINK) - - @property - def reload_code(self) -> str: - result = super().reload_code # ty: ignore[unresolved-attribute] - return dedent( - f""" - # the bundled site.py always adds the global site package if we're on python framework build, escape this - import sys - before = sys._framework - try: - sys._framework = None - {result} - finally: - sys._framework = before - """, - ) - - -def fix_mach_o(exe: str, current: str, new: str, max_size: int) -> None: - """https://en.wikipedia.org/wiki/Mach-O. - - Mach-O, short for Mach object file format, is a file format for executables, object code, shared libraries, - dynamically-loaded code, and core dumps. A replacement for the a.out format, Mach-O offers more extensibility and - faster access to information in the symbol table. - - Each Mach-O file is made up of one Mach-O header, followed by a series of load commands, followed by one or more - segments, each of which contains between 0 and 255 sections. Mach-O uses the REL relocation format to handle - references to symbols. When looking up symbols Mach-O uses a two-level namespace that encodes each symbol into an - 'object/symbol name' pair that is then linearly searched for by first the object and then the symbol name. - - The basic structureโ€”a list of variable-length "load commands" that reference pages of data elsewhere in the fileโ€”was - also used in the executable file format for Accent. The Accent file format was in turn, based on an idea from Spice - Lisp. - - With the introduction of Mac OS X 10.6 platform the Mach-O file underwent a significant modification that causes - binaries compiled on a computer running 10.6 or later to be (by default) executable only on computers running Mac OS - X 10.6 or later. The difference stems from load commands that the dynamic linker, in previous Mac OS X versions, - does not understand. Another significant change to the Mach-O format is the change in how the Link Edit tables - (found in the __LINKEDIT section) function. In 10.6 these new Link Edit tables are compressed by removing unused and - unneeded bits of information, however Mac OS X 10.5 and earlier cannot read this new Link Edit table format. - - """ - try: - LOGGER.debug("change Mach-O for %s from %s to %s", exe, current, new) - _builtin_change_mach_o(max_size)(exe, current, new) - except Exception as e: # noqa: BLE001 - LOGGER.warning("Could not call _builtin_change_mac_o: %s. Trying to call install_name_tool instead.", e) - try: - cmd = ["install_name_tool", "-change", current, new, exe] - subprocess.check_call(cmd) - except Exception: - logging.fatal("Could not call install_name_tool -- you must have Apple's development tools installed") - raise - - -def _builtin_change_mach_o(maxint: int) -> Callable[[str, str, str], None]: # noqa: C901 - MH_MAGIC = 0xFEEDFACE # noqa: N806 - MH_CIGAM = 0xCEFAEDFE # noqa: N806 - MH_MAGIC_64 = 0xFEEDFACF # noqa: N806 - MH_CIGAM_64 = 0xCFFAEDFE # noqa: N806 - FAT_MAGIC = 0xCAFEBABE # noqa: N806 - BIG_ENDIAN = ">" # noqa: N806 - LITTLE_ENDIAN = "<" # noqa: N806 - LC_LOAD_DYLIB = 0xC # noqa: N806 - - class FileView: - """A proxy for file-like objects that exposes a given view of a file. Modified from macholib.""" - - def __init__(self, file_obj: FileView | BufferedRandom, start: int = 0, size: int = maxint) -> None: - if isinstance(file_obj, FileView): - self._file_obj = file_obj._file_obj # noqa: SLF001 - else: - self._file_obj = file_obj - self._start = start - self._end = start + size - self._pos = 0 - - def __repr__(self) -> str: - return f"" - - def tell(self) -> int: - return self._pos - - def _checkwindow(self, seek_to: int, op: str) -> None: - if not (self._start <= seek_to <= self._end): - msg = f"{op} to offset {seek_to:d} is outside window [{self._start:d}, {self._end:d}]" - raise OSError(msg) - - def seek(self, offset: int, whence: int = 0) -> None: - seek_to = offset - if whence == os.SEEK_SET: - seek_to += self._start - elif whence == os.SEEK_CUR: - seek_to += self._start + self._pos - elif whence == os.SEEK_END: - seek_to += self._end - else: - msg = f"Invalid whence argument to seek: {whence!r}" - raise OSError(msg) - self._checkwindow(seek_to, "seek") - self._file_obj.seek(seek_to) - self._pos = seek_to - self._start - - def write(self, content: bytes) -> None: - here = self._start + self._pos - self._checkwindow(here, "write") - self._checkwindow(here + len(content), "write") - self._file_obj.seek(here, os.SEEK_SET) - self._file_obj.write(content) - self._pos += len(content) - - def read(self, size: int = maxint) -> bytes: - assert size >= 0 # noqa: S101 - here = self._start + self._pos - self._checkwindow(here, "read") - size = min(size, self._end - here) - self._file_obj.seek(here, os.SEEK_SET) - read_bytes = self._file_obj.read(size) - self._pos += len(read_bytes) - return read_bytes - - def read_data(file: FileView, endian: str, num: int = 1) -> int | tuple[int, ...]: - """Read a given number of 32-bits unsigned integers from the given file with the given endianness.""" - res = struct.unpack(endian + "L" * num, file.read(num * 4)) - if len(res) == 1: - return res[0] - return res - - def mach_o_change(at_path: str, what: str, value: str) -> None: # noqa: C901 - """Replace a given name (what) in any LC_LOAD_DYLIB command found in the given binary with a new name (value), provided it's shorter.""" - - def do_macho(file: FileView, bits: int, endian: str) -> None: - # Read Mach-O header (the magic number is assumed read by the caller) - _cpu_type, _cpu_sub_type, _file_type, n_commands, _size_of_commands, _flags = read_data(file, endian, 6) # ty: ignore[not-iterable] - # 64-bits header has one more field. - if bits == 64: # noqa: PLR2004 - read_data(file, endian) - # The header is followed by n commands - for _ in range(n_commands): - where = file.tell() - # Read command header - cmd, cmd_size = read_data(file, endian, 2) # ty: ignore[not-iterable] - if cmd == LC_LOAD_DYLIB: - # The first data field in LC_LOAD_DYLIB commands is the offset of the name, starting from the - # beginning of the command. - name_offset = read_data(file, endian) - file.seek(where + name_offset, os.SEEK_SET) # ty: ignore[unsupported-operator] - # Read the NUL terminated string - load = file.read(cmd_size - name_offset).decode() # ty: ignore[unsupported-operator] - load = load[: load.index("\0")] - # If the string is what is being replaced, overwrite it. - if load == what: - file.seek(where + name_offset, os.SEEK_SET) # ty: ignore[unsupported-operator] - file.write(value.encode() + b"\0") - # Seek to the next command - file.seek(where + cmd_size, os.SEEK_SET) - - def do_file(file: FileView | BufferedRandom, offset: int = 0, size: int = maxint) -> None: - file = FileView(file, offset, size) - # Read magic number - magic = read_data(file, BIG_ENDIAN) - if magic == FAT_MAGIC: - # Fat binaries contain nfat_arch Mach-O binaries - n_fat_arch = read_data(file, BIG_ENDIAN) - for _ in range(n_fat_arch): # ty: ignore[invalid-argument-type] - # Read arch header - _cpu_type, _cpu_sub_type, offset, size, _align = read_data(file, BIG_ENDIAN, 5) # ty: ignore[not-iterable] - do_file(file, offset, size) - elif magic == MH_MAGIC: - do_macho(file, 32, BIG_ENDIAN) - elif magic == MH_CIGAM: - do_macho(file, 32, LITTLE_ENDIAN) - elif magic == MH_MAGIC_64: - do_macho(file, 64, BIG_ENDIAN) - elif magic == MH_CIGAM_64: - do_macho(file, 64, LITTLE_ENDIAN) - - assert len(what) >= len(value) # noqa: S101 - - with open(at_path, "r+b") as f: - do_file(f) - - return mach_o_change - - -class CPython3macOsBrew(CPython3, CPythonPosix): - @classmethod - def can_describe(cls, interpreter: PythonInfo) -> bool: - return is_macos_brew(interpreter) and super().can_describe(interpreter) - - @classmethod - def setup_meta(cls, interpreter: PythonInfo) -> BuiltinViaGlobalRefMeta: # noqa: ARG003 - meta = BuiltinViaGlobalRefMeta() - meta.copy_error = "Brew disables copy creation: https://github.com/Homebrew/homebrew-core/issues/138159" - return meta - - -__all__ = [ - "CPython3macOsBrew", - "CPython3macOsFramework", - "CPythonmacOsFramework", -] diff --git a/src/virtualenv/create/via_global_ref/builtin/graalpy/__init__.py b/src/virtualenv/create/via_global_ref/builtin/graalpy/__init__.py deleted file mode 100644 index d949a3355..000000000 --- a/src/virtualenv/create/via_global_ref/builtin/graalpy/__init__.py +++ /dev/null @@ -1,99 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from pathlib import Path -from typing import TYPE_CHECKING - -from virtualenv.create.describe import PosixSupports, WindowsSupports -from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest, RefMust, RefWhen -from virtualenv.create.via_global_ref.builtin.via_global_self_do import ViaGlobalRefVirtualenvBuiltin - -if TYPE_CHECKING: - from collections.abc import Generator, Iterator - - from python_discovery import PythonInfo - - -class GraalPy(ViaGlobalRefVirtualenvBuiltin, ABC): - @classmethod - @abstractmethod - def _native_lib(cls, lib_dir: Path, platform: str) -> Path: - """Return the path to the native library for this platform.""" - raise NotImplementedError - - @classmethod - def can_describe(cls, interpreter: PythonInfo) -> bool: - return interpreter.implementation == "GraalVM" and super().can_describe(interpreter) - - @classmethod - def exe_stem(cls) -> str: - return "graalpy" - - @classmethod - def exe_names(cls, interpreter: PythonInfo) -> set[str]: - return { - cls.exe_stem(), - "python", - f"python{interpreter.version_info.major}", - f"python{interpreter.version_info.major}.{interpreter.version_info.minor}", - } - - @classmethod - def _executables(cls, interpreter: PythonInfo) -> Generator[tuple[Path, list[str], RefMust, RefWhen]]: # ty: ignore[invalid-method-override] - host = Path(interpreter.system_executable) # ty: ignore[invalid-argument-type] - targets = sorted(f"{name}{cls.suffix}" for name in cls.exe_names(interpreter)) - yield host, targets, RefMust.NA, RefWhen.ANY - - @classmethod - def sources(cls, interpreter: PythonInfo) -> Generator[PathRefToDest]: # ty: ignore[invalid-method-override] - yield from super().sources(interpreter) - python_dir = Path(interpreter.system_executable).resolve().parent # ty: ignore[invalid-argument-type] - if python_dir.name in {"bin", "Scripts"}: - python_dir = python_dir.parent - - native_lib = cls._native_lib(python_dir / "lib", interpreter.platform) - if native_lib.exists(): - yield PathRefToDest(native_lib, dest=lambda self, s: self.bin_dir.parent / "lib" / s.name) - - for jvm_dir_name in ("jvm", "jvmlibs", "modules"): - jvm_dir = python_dir / jvm_dir_name - if jvm_dir.exists(): - yield PathRefToDest(jvm_dir, dest=lambda self, s: self.bin_dir.parent / s.name) - - @classmethod - def _shared_libs(cls, python_dir: Path) -> Iterator[Path]: - raise NotImplementedError - - def set_pyenv_cfg(self) -> None: - super().set_pyenv_cfg() - # GraalPy 24.0 and older had home without the bin - version = self.interpreter.version_info - if version.minor <= 10: # noqa: PLR2004 - home = Path(self.pyenv_cfg["home"]) - if home.name == "bin": - self.pyenv_cfg["home"] = str(home.parent) - - -class GraalPyPosix(GraalPy, PosixSupports): - @classmethod - def _native_lib(cls, lib_dir: Path, platform: str) -> Path: - if platform == "darwin": - return lib_dir / "libpythonvm.dylib" - return lib_dir / "libpythonvm.so" - - -class GraalPyWindows(GraalPy, WindowsSupports): - @classmethod - def _native_lib(cls, lib_dir: Path, platform: str) -> Path: # noqa: ARG003 - return lib_dir / "pythonvm.dll" - - def set_pyenv_cfg(self) -> None: - # GraalPy needs an additional entry in pyvenv.cfg on Windows - super().set_pyenv_cfg() - self.pyenv_cfg["venvlauncher_command"] = self.interpreter.system_executable # ty: ignore[invalid-assignment] - - -__all__ = [ - "GraalPyPosix", - "GraalPyWindows", -] diff --git a/src/virtualenv/create/via_global_ref/builtin/pypy/__init__.py b/src/virtualenv/create/via_global_ref/builtin/pypy/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/virtualenv/create/via_global_ref/builtin/pypy/common.py b/src/virtualenv/create/via_global_ref/builtin/pypy/common.py deleted file mode 100644 index 4bf391107..000000000 --- a/src/virtualenv/create/via_global_ref/builtin/pypy/common.py +++ /dev/null @@ -1,61 +0,0 @@ -from __future__ import annotations - -import abc -from pathlib import Path -from typing import TYPE_CHECKING - -from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest, RefMust, RefWhen -from virtualenv.create.via_global_ref.builtin.via_global_self_do import ViaGlobalRefVirtualenvBuiltin - -if TYPE_CHECKING: - from collections.abc import Generator, Iterator - - from python_discovery import PythonInfo - - from virtualenv.create.via_global_ref.builtin.ref import PathRef - - -class PyPy(ViaGlobalRefVirtualenvBuiltin, abc.ABC): - @classmethod - def can_describe(cls, interpreter: PythonInfo) -> bool: - return interpreter.implementation == "PyPy" and super().can_describe(interpreter) - - @classmethod - def _executables(cls, interpreter: PythonInfo) -> Generator[tuple[Path, list[str], str, str]]: - host = Path(interpreter.system_executable) # ty: ignore[invalid-argument-type] - targets = sorted(f"{name}{PyPy.suffix}" for name in cls.exe_names(interpreter)) - yield host, targets, RefMust.NA, RefWhen.ANY - - @classmethod - def executables(cls, interpreter: PythonInfo) -> Generator[PathRef]: - yield from super().sources(interpreter) - - @classmethod - def exe_names(cls, interpreter: PythonInfo) -> set[str]: - return { - cls.exe_stem(), - "python", - f"python{interpreter.version_info.major}", - f"python{interpreter.version_info.major}.{interpreter.version_info.minor}", - } - - @classmethod - def sources(cls, interpreter: PythonInfo) -> Generator[PathRef]: # ty: ignore[invalid-method-override] - yield from cls.executables(interpreter) - for host in cls._add_shared_libs(interpreter): - yield PathRefToDest(host, dest=lambda self, s: self.bin_dir / s.name) - - @classmethod - def _add_shared_libs(cls, interpreter: PythonInfo) -> Generator[Path]: - # https://bitbucket.org/pypy/pypy/issue/1922/future-proofing-virtualenv - python_dir = Path(interpreter.system_executable).resolve().parent # ty: ignore[invalid-argument-type] - yield from cls._shared_libs(python_dir) - - @classmethod - def _shared_libs(cls, python_dir: Path) -> Iterator[Path]: - raise NotImplementedError - - -__all__ = [ - "PyPy", -] diff --git a/src/virtualenv/create/via_global_ref/builtin/pypy/pypy3.py b/src/virtualenv/create/via_global_ref/builtin/pypy/pypy3.py deleted file mode 100644 index 69a6e607a..000000000 --- a/src/virtualenv/create/via_global_ref/builtin/pypy/pypy3.py +++ /dev/null @@ -1,83 +0,0 @@ -from __future__ import annotations - -import abc -from pathlib import Path -from typing import TYPE_CHECKING - -from virtualenv.create.describe import PosixSupports, Python3Supports, WindowsSupports -from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest - -from .common import PyPy - -if TYPE_CHECKING: - from collections.abc import Generator, Iterator - - from python_discovery import PythonInfo - - from virtualenv.create.via_global_ref.builtin.ref import PathRef - - -class PyPy3(PyPy, Python3Supports, abc.ABC): - @classmethod - def exe_stem(cls) -> str: - return "pypy3" - - @classmethod - def exe_names(cls, interpreter: PythonInfo) -> set[str]: - return super().exe_names(interpreter) | {"pypy"} - - -class PyPy3Posix(PyPy3, PosixSupports): - """PyPy 3 on POSIX.""" - - @classmethod - def _shared_libs(cls, python_dir: Path) -> Iterator[Path]: - # glob for libpypy3-c.so, libpypy3-c.dylib, libpypy3.9-c.so ... - return python_dir.glob("libpypy3*.*") - - def to_lib(self, src: Path) -> Path: - return self.dest / "lib" / src.name - - @classmethod - def sources(cls, interpreter: PythonInfo) -> Generator[PathRef]: - yield from super().sources(interpreter) - # PyPy >= 3.8 supports a standard prefix installation, where older versions always used a portable/development - # style installation. If this is a standard prefix installation, skip the below: - if interpreter.system_prefix == "/usr": - return - # Also copy/symlink anything under prefix/lib, which, for "portable" PyPy builds, includes the tk,tcl runtime - # and a number of shared objects. In distro-specific builds or on conda this should be empty (on PyPy3.8+ it - # will, like on CPython, hold the stdlib). - host_lib = Path(interpreter.system_prefix) / "lib" - stdlib = Path(interpreter.system_stdlib) - if host_lib.exists() and host_lib.is_dir(): - if (deps_file := host_lib / "PYPY_PORTABLE_DEPS.txt").exists(): - for line in deps_file.read_text(encoding="utf-8").splitlines(): - dep = line.strip() - if dep and (path := host_lib / dep).exists(): - yield PathRefToDest(path, dest=cls.to_lib) - else: - for path in host_lib.iterdir(): - if stdlib == path: - continue - yield PathRefToDest(path, dest=cls.to_lib) - - -class Pypy3Windows(PyPy3, WindowsSupports): - """PyPy 3 on Windows.""" - - @property - def less_v37(self) -> bool: - return self.interpreter.version_info.minor < 7 # noqa: PLR2004 - - @classmethod - def _shared_libs(cls, python_dir: Path) -> Iterator[Path]: - # PyPy does not use a PEP 397 launcher, so all DLLs from the interpreter directory are needed for the venv - yield from python_dir.glob("*.dll") - - -__all__ = [ - "PyPy3", - "PyPy3Posix", - "Pypy3Windows", -] diff --git a/src/virtualenv/create/via_global_ref/builtin/ref.py b/src/virtualenv/create/via_global_ref/builtin/ref.py deleted file mode 100644 index 767c2a280..000000000 --- a/src/virtualenv/create/via_global_ref/builtin/ref.py +++ /dev/null @@ -1,181 +0,0 @@ -"""Virtual environments in the traditional sense are built as reference to the host python. This file allows declarative references to elements on the file system, allowing our system to automatically detect what modes it can support given the constraints: e.g. can the file system symlink, can the files be read, executed, etc.""" - -from __future__ import annotations - -import os -from abc import ABC, abstractmethod -from collections import OrderedDict -from stat import S_IXGRP, S_IXOTH, S_IXUSR -from typing import TYPE_CHECKING - -from virtualenv.info import fs_is_case_sensitive, fs_supports_symlink -from virtualenv.util.path import copy, make_exe, symlink - -if TYPE_CHECKING: - from collections.abc import Callable - from pathlib import Path - - -class RefMust: - NA = "NA" - COPY = "copy" - SYMLINK = "symlink" - - -class RefWhen: - ANY = "ANY" - COPY = "copy" - SYMLINK = "symlink" - - -class PathRef(ABC): - """Base class that checks if a file reference can be symlink/copied.""" - - FS_SUPPORTS_SYMLINK = fs_supports_symlink() - FS_CASE_SENSITIVE = fs_is_case_sensitive() - - def __init__(self, src: Path, must: str = RefMust.NA, when: str = RefWhen.ANY) -> None: - self.must = must - self.when = when - self.src = src - try: - self.exists = src.exists() - except OSError: - self.exists = False - self._can_read: bool | None = None if self.exists else False - self._can_copy: bool | None = None if self.exists else False - self._can_symlink: bool | None = None if self.exists else False - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(src={self.src})" - - @property - def can_read(self) -> bool: - if self._can_read is None: - if self.src.is_file(): - try: - with self.src.open("rb"): - self._can_read = True - except OSError: - self._can_read = False - else: - self._can_read = os.access(str(self.src), os.R_OK) - return self._can_read - - @property - def can_copy(self) -> bool: - if self._can_copy is None: - if self.must == RefMust.SYMLINK: - self._can_copy = self.can_symlink - else: - self._can_copy = self.can_read - return self._can_copy - - @property - def can_symlink(self) -> bool: - if self._can_symlink is None: - if self.must == RefMust.COPY: - self._can_symlink = self.can_copy - else: - self._can_symlink = self.FS_SUPPORTS_SYMLINK and self.can_read - return self._can_symlink - - @abstractmethod - def run(self, creator: object, symlinks: bool) -> None: - raise NotImplementedError - - def method(self, symlinks: bool) -> Callable[..., None]: - if self.must == RefMust.SYMLINK: - return symlink - if self.must == RefMust.COPY: - return copy - return symlink if symlinks else copy - - -class ExePathRef(PathRef, ABC): - """Base class that checks if a executable can be references via symlink/copy.""" - - def __init__(self, src: Path, must: str = RefMust.NA, when: str = RefWhen.ANY) -> None: - super().__init__(src, must, when) - self._can_run: bool | None = None - - @property - def can_symlink(self) -> bool: - if self.FS_SUPPORTS_SYMLINK: - return self.can_run - return False - - @property - def can_run(self) -> bool: - if self._can_run is None: - mode = self.src.stat().st_mode - for key in [S_IXUSR, S_IXGRP, S_IXOTH]: - if mode & key: - self._can_run = True - break - else: - self._can_run = False - return self._can_run # ty: ignore[invalid-return-type] - - -class PathRefToDest(PathRef): - """Link a path on the file system.""" - - def __init__(self, src: Path, dest: Callable[..., Path], must: str = RefMust.NA, when: str = RefWhen.ANY) -> None: - super().__init__(src, must, when) - self.dest = dest - - def run(self, creator: object, symlinks: bool) -> None: - dest = self.dest(creator, self.src) - method = self.method(symlinks) - dest_iterable = dest if isinstance(dest, list) else (dest,) - if not dest.parent.exists(): - dest.parent.mkdir(parents=True, exist_ok=True) - for dst in dest_iterable: - method(self.src, dst) - - -class ExePathRefToDest(PathRefToDest, ExePathRef): - """Link a exe path on the file system.""" - - def __init__( - self, src: Path, targets: list[str], dest: Callable[..., Path], must: str = RefMust.NA, when: str = RefWhen.ANY - ) -> None: - ExePathRef.__init__(self, src, must, when) - PathRefToDest.__init__(self, src, dest, must, when) - if not self.FS_CASE_SENSITIVE: - targets = list(OrderedDict((i.lower(), None) for i in targets).keys()) - self.base = targets[0] - self.aliases = targets[1:] - self.dest = dest - - def run(self, creator: object, symlinks: bool) -> None: - bin_dir = self.dest(creator, self.src).parent - dest = bin_dir / self.base - method = self.method(symlinks) - method(self.src, dest) - if not symlinks: - make_exe(dest) - for extra in self.aliases: - link_file = bin_dir / extra - if link_file.exists(): - link_file.unlink() - if symlinks: - link_file.symlink_to(self.base) - else: - copy(self.src, link_file) - if not symlinks: - make_exe(link_file) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(src={self.src}, alias={self.aliases})" - - -__all__ = [ - "ExePathRef", - "ExePathRefToDest", - "PathRef", - "PathRefToDest", - "RefMust", - "RefWhen", -] diff --git a/src/virtualenv/create/via_global_ref/builtin/rustpython/__init__.py b/src/virtualenv/create/via_global_ref/builtin/rustpython/__init__.py deleted file mode 100644 index 899533841..000000000 --- a/src/virtualenv/create/via_global_ref/builtin/rustpython/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import annotations - -from abc import ABC -from pathlib import Path -from typing import TYPE_CHECKING - -from virtualenv.create.describe import PosixSupports, Python3Supports, WindowsSupports -from virtualenv.create.via_global_ref.builtin.ref import RefMust, RefWhen -from virtualenv.create.via_global_ref.builtin.via_global_self_do import ViaGlobalRefVirtualenvBuiltin - -if TYPE_CHECKING: - from collections.abc import Generator - - from python_discovery import PythonInfo - - -class RustPython(ViaGlobalRefVirtualenvBuiltin, Python3Supports, ABC): - @classmethod - def can_describe(cls, interpreter: PythonInfo) -> bool: - return interpreter.implementation == "RustPython" and super().can_describe(interpreter) - - @classmethod - def exe_stem(cls) -> str: - return "rustpython" - - @classmethod - def exe_names(cls, interpreter: PythonInfo) -> set[str]: - return { - cls.exe_stem(), - "python", - f"python{interpreter.version_info.major}", - f"python{interpreter.version_info.major}.{interpreter.version_info.minor}", - } - - @classmethod - def _executables(cls, interpreter: PythonInfo) -> Generator[tuple[Path, list[str], RefMust, RefWhen]]: # ty: ignore[invalid-method-override] - host = Path(interpreter.system_executable) # ty: ignore[invalid-argument-type] - targets = sorted(f"{name}{cls.suffix}" for name in cls.exe_names(interpreter)) - yield host, targets, RefMust.NA, RefWhen.ANY - - -class RustPythonPosix(RustPython, PosixSupports): - """RustPython on POSIX.""" - - -class RustPythonWindows(RustPython, WindowsSupports): - """RustPython on Windows.""" - - -__all__ = [ - "RustPythonPosix", - "RustPythonWindows", -] diff --git a/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py b/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py deleted file mode 100644 index f86861e63..000000000 --- a/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py +++ /dev/null @@ -1,135 +0,0 @@ -from __future__ import annotations - -from abc import ABC -from typing import TYPE_CHECKING - -from virtualenv.create.via_global_ref.api import ViaGlobalRefApi, ViaGlobalRefMeta -from virtualenv.create.via_global_ref.builtin.ref import ( - ExePathRefToDest, - RefMust, - RefWhen, -) -from virtualenv.util.path import ensure_dir - -from .builtin_way import VirtualenvBuiltin - -if TYPE_CHECKING: - from collections.abc import Generator - from pathlib import Path - - from python_discovery import PythonInfo - - from virtualenv.config.cli.parser import VirtualEnvOptions - from virtualenv.create.via_global_ref.builtin.ref import PathRef - from virtualenv.create.via_global_ref.venv import Venv - - -class BuiltinViaGlobalRefMeta(ViaGlobalRefMeta): - def __init__(self) -> None: - super().__init__() - self.sources: list[PathRef] = [] - - -class ViaGlobalRefVirtualenvBuiltin(ViaGlobalRefApi, VirtualenvBuiltin, ABC): - def __init__(self, options: VirtualEnvOptions, interpreter: PythonInfo) -> None: - super().__init__(options, interpreter) - self._sources: list[PathRef] = ( - getattr(options.meta, "sources", None) or [] - ) # if created as a describer this might be missing - - @classmethod - def can_create(cls, interpreter: PythonInfo) -> BuiltinViaGlobalRefMeta | None: - """By default, all built-in methods assume that if we can describe it we can create it.""" - # first we must be able to describe it - if not cls.can_describe(interpreter): - return None - meta = cls.setup_meta(interpreter) - if meta is not None and meta: - cls._sources_can_be_applied(interpreter, meta) - return meta - - @classmethod - def _sources_can_be_applied(cls, interpreter: PythonInfo, meta: BuiltinViaGlobalRefMeta) -> None: - for src in cls.sources(interpreter): - if src.exists: - if meta.can_copy and not src.can_copy: - meta.copy_error = f"cannot copy {src}" - if meta.can_symlink and not src.can_symlink: - meta.symlink_error = f"cannot symlink {src}" - else: - msg = f"missing required file {src}" - if src.when == RefMust.NA: - meta.error = msg - elif src.when == RefMust.COPY: - meta.copy_error = msg - elif src.when == RefMust.SYMLINK: - meta.symlink_error = msg - if not meta.can_copy and not meta.can_symlink: - meta.error = f"neither copy or symlink supported, copy: {meta.copy_error} symlink: {meta.symlink_error}" - if meta.error: - break - meta.sources.append(src) - - @classmethod - def setup_meta(cls, interpreter: PythonInfo) -> BuiltinViaGlobalRefMeta: # noqa: ARG003 - return BuiltinViaGlobalRefMeta() - - @classmethod - def sources(cls, interpreter: PythonInfo) -> Generator[ExePathRefToDest]: - for host_exe, targets, must, when in cls._executables(interpreter): - yield ExePathRefToDest(host_exe, dest=cls.to_bin, targets=targets, must=must, when=when) - - def to_bin(self, src: Path) -> Path: - return self.bin_dir / src.name - - @classmethod - def _executables(cls, interpreter: PythonInfo) -> Generator[tuple[Path, list[str], str, str]]: - raise NotImplementedError - - def create(self) -> None: - dirs = self.ensure_directories() - for directory in list(dirs): - if any(i for i in dirs if i is not directory and directory.parts == i.parts[: len(directory.parts)]): - dirs.remove(directory) - for directory in sorted(dirs): - ensure_dir(directory) - - self.set_pyenv_cfg() - self.pyenv_cfg.write() - true_system_site = self.enable_system_site_package - try: - self.enable_system_site_package = False - for src in self._sources: - if ( - src.when == RefWhen.ANY - or (src.when == RefWhen.SYMLINK and self.symlinks is True) - or (src.when == RefWhen.COPY and self.symlinks is False) - ): - src.run(self, self.symlinks) - finally: - if true_system_site != self.enable_system_site_package: - self.enable_system_site_package = true_system_site - super().create() - - @property - def include_dir(self) -> Path: - return self.dest / ("Include" if self.interpreter.os == "nt" else "include") - - def install_venv_shared_libs(self, venv_creator: Venv) -> None: - pass - - def ensure_directories(self) -> set[Path]: - return {self.dest, self.bin_dir, self.script_dir, self.stdlib, self.include_dir} | set(self.libs) - - def set_pyenv_cfg(self) -> None: - """We directly inject the base prefix and base exec prefix to avoid site.py needing to discover these from home (which usually is done within the interpreter itself).""" - super().set_pyenv_cfg() - self.pyenv_cfg["base-prefix"] = self.interpreter.system_prefix - self.pyenv_cfg["base-exec-prefix"] = self.interpreter.system_exec_prefix - self.pyenv_cfg["base-executable"] = self.interpreter.system_executable # ty: ignore[invalid-assignment] - - -__all__ = [ - "BuiltinViaGlobalRefMeta", - "ViaGlobalRefVirtualenvBuiltin", -] diff --git a/src/virtualenv/create/via_global_ref/store.py b/src/virtualenv/create/via_global_ref/store.py deleted file mode 100644 index db9d475ad..000000000 --- a/src/virtualenv/create/via_global_ref/store.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from python_discovery import PythonInfo - - from virtualenv.create.via_global_ref.api import ViaGlobalRefMeta - - -def handle_store_python(meta: ViaGlobalRefMeta, interpreter: PythonInfo) -> ViaGlobalRefMeta: - if is_store_python(interpreter): - meta.symlink_error = "Windows Store Python does not support virtual environments via symlink" - return meta - - -def is_store_python(interpreter: PythonInfo) -> bool: - parts = Path(interpreter.system_executable).parts # ty: ignore[invalid-argument-type] - return ( - len(parts) > 4 # noqa: PLR2004 - and parts[-4] == "Microsoft" - and parts[-3] == "WindowsApps" - and parts[-2].startswith("PythonSoftwareFoundation.Python.3.") - and parts[-1].startswith("python") - ) - - -__all__ = [ - "handle_store_python", - "is_store_python", -] diff --git a/src/virtualenv/create/via_global_ref/venv.py b/src/virtualenv/create/via_global_ref/venv.py deleted file mode 100644 index 9c95d12b9..000000000 --- a/src/virtualenv/create/via_global_ref/venv.py +++ /dev/null @@ -1,114 +0,0 @@ -from __future__ import annotations - -import logging -from copy import copy -from typing import TYPE_CHECKING - -from python_discovery import PythonInfo - -from virtualenv.create.via_global_ref.store import handle_store_python -from virtualenv.util.error import ProcessCallFailedError -from virtualenv.util.path import ensure_dir -from virtualenv.util.subprocess import run_cmd - -from .api import ViaGlobalRefApi, ViaGlobalRefMeta -from .builtin.cpython.common import is_mac_os_framework -from .builtin.cpython.mac_os import CPython3macOsBrew -from .builtin.pypy.pypy3 import Pypy3Windows - -if TYPE_CHECKING: - from typing import Any - - from virtualenv.config.cli.parser import VirtualEnvOptions - -LOGGER = logging.getLogger(__name__) - - -class Venv(ViaGlobalRefApi): - def __init__(self, options: VirtualEnvOptions, interpreter: PythonInfo) -> None: - self.describe = options.describe - super().__init__(options, interpreter) - current = PythonInfo.current() - self.can_be_inline = interpreter is current and interpreter.executable == interpreter.system_executable - self._context = None - - def _args(self) -> list[tuple[str, Any]]: - return super()._args() + ([("describe", self.describe.__class__.__name__)] if self.describe else []) - - @classmethod - def can_create(cls, interpreter: PythonInfo) -> ViaGlobalRefMeta | None: - if interpreter.has_venv: - if CPython3macOsBrew.can_describe(interpreter): - return CPython3macOsBrew.setup_meta(interpreter) - meta = ViaGlobalRefMeta() - if interpreter.platform == "win32": - meta = handle_store_python(meta, interpreter) - if is_mac_os_framework(interpreter): - meta.copy_error = "macOS framework builds do not support copy-based virtual environments" - return meta - return None - - def create(self) -> None: - if self.can_be_inline: - self.create_inline() - else: - self.create_via_sub_process() - for lib in self.libs: # ty: ignore[not-iterable] - ensure_dir(lib) - if self.describe is not None: - self.describe.install_venv_shared_libs(self) - super().create() - self.executables_for_win_pypy_less_v37() - - def executables_for_win_pypy_less_v37(self) -> None: - """PyPy <= 3.6 (v7.3.3) for Windows contains only pypy3.exe and pypy3w.exe Venv does not handle non-existing exe sources, e.g. python.exe, so this patch does it.""" - creator = self.describe - if isinstance(creator, Pypy3Windows) and creator.less_v37: - for exe in creator.executables(self.interpreter): - exe.run(creator, self.symlinks) - - def create_inline(self) -> None: - from venv import EnvBuilder # noqa: PLC0415 - - builder = EnvBuilder( - system_site_packages=self.enable_system_site_package, - clear=False, - symlinks=self.symlinks, - with_pip=False, - ) - builder.create(str(self.dest)) - - def create_via_sub_process(self) -> None: - cmd = self.get_host_create_cmd() - LOGGER.info("using host built-in venv to create via %s", " ".join(cmd)) - code, out, err = run_cmd(cmd) - if code != 0: - raise ProcessCallFailedError(code, out, err, cmd) - - def get_host_create_cmd(self) -> list[str]: - cmd = [self.interpreter.system_executable, "-m", "venv", "--without-pip"] - if self.interpreter.version_info >= (3, 13): - cmd.append("--without-scm-ignore-files") - if self.enable_system_site_package: - cmd.append("--system-site-packages") - cmd.extend(("--symlinks" if self.symlinks else "--copies", str(self.dest))) - return cmd # ty: ignore[invalid-return-type] - - def set_pyenv_cfg(self) -> None: - # prefer venv options over ours, but keep our extra - venv_content = copy(self.pyenv_cfg.refresh()) - super().set_pyenv_cfg() - self.pyenv_cfg.update(venv_content) - - def __getattribute__(self, item: str) -> object: - describe = object.__getattribute__(self, "describe") - if describe is not None and hasattr(describe, item): - element = getattr(describe, item) - if not callable(element) or item == "script": - return element - return object.__getattribute__(self, item) - - -__all__ = [ - "Venv", -] diff --git a/src/virtualenv/discovery/__init__.py b/src/virtualenv/discovery/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/virtualenv/discovery/builtin.py b/src/virtualenv/discovery/builtin.py deleted file mode 100644 index 395e77116..000000000 --- a/src/virtualenv/discovery/builtin.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Virtualenv-specific Builtin discovery wrapping py_discovery.""" - -from __future__ import annotations - -import sys -from typing import TYPE_CHECKING - -from python_discovery import get_interpreter as _get_interpreter - -from .discover import Discover - -if TYPE_CHECKING: - from argparse import ArgumentParser - from collections.abc import Iterable, Mapping, Sequence - - from python_discovery import PyInfoCache, PythonInfo - - from virtualenv.config.cli.parser import VirtualEnvOptions - - -def get_interpreter( - key: str, - try_first_with: Iterable[str], - cache: PyInfoCache | None = None, - env: Mapping[str, str] | None = None, - app_data: PyInfoCache | None = None, -) -> PythonInfo | None: - return _get_interpreter(key, try_first_with, cache or app_data, env) - - -class Builtin(Discover): - python_spec: Sequence[str] - app_data: PyInfoCache - try_first_with: Sequence[str] - - def __init__(self, options: VirtualEnvOptions) -> None: - super().__init__(options) - self.python_spec = options.python or [sys.executable] - if self._env.get("VIRTUALENV_PYTHON"): - self.python_spec = self.python_spec[1:] + self.python_spec[:1] - self.app_data = options.app_data - self.try_first_with = options.try_first_with - - @classmethod - def add_parser_arguments(cls, parser: ArgumentParser) -> None: - parser.add_argument( - "-p", - "--python", - dest="python", - metavar="py", - type=str, - action="append", - default=[], - help="interpreter based on what to create environment (path/identifier/version-specifier) " - "- by default use the interpreter where the tool is installed - first found wins. " - "Version specifiers (e.g., >=3.12, ~=3.11.0, ==3.10) are also supported", - ) - parser.add_argument( - "--try-first-with", - dest="try_first_with", - metavar="py_exe", - type=str, - action="append", - default=[], - help="try first these interpreters before starting the discovery", - ) - - def run(self) -> PythonInfo | None: - for python_spec in self.python_spec: - if result := get_interpreter( - python_spec, - self.try_first_with, - app_data=self.app_data, - env=self._env, - ): - return result - return None - - def __repr__(self) -> str: - spec = self.python_spec[0] if len(self.python_spec) == 1 else self.python_spec - return f"{self.__class__.__name__} discover of python_spec={spec!r}" - - -__all__ = [ - "Builtin", - "get_interpreter", -] diff --git a/src/virtualenv/discovery/cached_py_info.py b/src/virtualenv/discovery/cached_py_info.py deleted file mode 100644 index 76341c52a..000000000 --- a/src/virtualenv/discovery/cached_py_info.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Backward-compatibility re-export โ€” use ``python_discovery`` directly.""" - -from __future__ import annotations - -from python_discovery._cached_py_info import clear, from_exe # noqa: PLC2701 - -__all__ = [ - "clear", - "from_exe", -] diff --git a/src/virtualenv/discovery/discover.py b/src/virtualenv/discovery/discover.py deleted file mode 100644 index 79ccab740..000000000 --- a/src/virtualenv/discovery/discover.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Virtualenv-specific Discover base class for plugin-based Python discovery.""" - -from __future__ import annotations - -import os -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from argparse import ArgumentParser - from collections.abc import Mapping - - from python_discovery import PythonInfo - - from virtualenv.config.cli.parser import VirtualEnvOptions - - -class Discover(ABC): - @classmethod - def add_parser_arguments(cls, parser: ArgumentParser) -> None: - raise NotImplementedError - - def __init__(self, options: VirtualEnvOptions) -> None: - self._has_run = False - self._interpreter: PythonInfo | None = None - self._env: Mapping[str, str] = options.env if options.env is not None else os.environ - - @abstractmethod - def run(self) -> PythonInfo | None: - raise NotImplementedError - - @property - def interpreter(self) -> PythonInfo | None: - """:returns: the interpreter as returned by :meth:`run`, cached""" - if self._has_run is False: - self._interpreter = self.run() - self._has_run = True - return self._interpreter - - -__all__ = [ - "Discover", -] diff --git a/src/virtualenv/discovery/py_info.py b/src/virtualenv/discovery/py_info.py deleted file mode 100644 index 511586593..000000000 --- a/src/virtualenv/discovery/py_info.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Backward-compatibility re-export โ€” use ``python_discovery.PythonInfo`` directly.""" - -from __future__ import annotations - -from python_discovery import PythonInfo - -__all__ = [ - "PythonInfo", -] diff --git a/src/virtualenv/discovery/py_spec.py b/src/virtualenv/discovery/py_spec.py deleted file mode 100644 index c407f6e0f..000000000 --- a/src/virtualenv/discovery/py_spec.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Backward-compatibility re-export โ€” use ``python_discovery.PythonSpec`` directly.""" - -from __future__ import annotations - -from python_discovery import PythonSpec - -__all__ = [ - "PythonSpec", -] diff --git a/src/virtualenv/info.py b/src/virtualenv/info.py deleted file mode 100644 index 4ba74a12a..000000000 --- a/src/virtualenv/info.py +++ /dev/null @@ -1,72 +0,0 @@ -from __future__ import annotations - -import logging -import os -import platform -import sys -import tempfile - -IMPLEMENTATION = platform.python_implementation() -IS_PYPY = IMPLEMENTATION == "PyPy" -IS_GRAALPY = IMPLEMENTATION == "GraalVM" -IS_RUSTPYTHON = IMPLEMENTATION == "RustPython" -IS_CPYTHON = IMPLEMENTATION == "CPython" -IS_WIN = sys.platform == "win32" -IS_MAC_ARM64 = sys.platform == "darwin" and platform.machine() == "arm64" -ROOT = os.path.realpath(os.path.join(os.path.abspath(__file__), os.path.pardir, os.path.pardir)) -IS_ZIPAPP = os.path.isfile(ROOT) -_CAN_SYMLINK = _FS_CASE_SENSITIVE = _CFG_DIR = _DATA_DIR = None -LOGGER = logging.getLogger(__name__) - - -def fs_is_case_sensitive() -> bool: - global _FS_CASE_SENSITIVE # noqa: PLW0603 - - if _FS_CASE_SENSITIVE is None: - with tempfile.NamedTemporaryFile(prefix="TmP") as tmp_file: - _FS_CASE_SENSITIVE = not os.path.exists(tmp_file.name.lower()) - LOGGER.debug("filesystem is %scase-sensitive", "" if _FS_CASE_SENSITIVE else "not ") - return _FS_CASE_SENSITIVE - - -def fs_supports_symlink() -> bool: - global _CAN_SYMLINK # noqa: PLW0603 - - if _CAN_SYMLINK is None: - can = False - if hasattr(os, "symlink"): - # Creating a symlink can fail for a variety of reasons, indicating that the filesystem does not support it. - # E.g. on Linux with a VFAT partition mounted. - with tempfile.NamedTemporaryFile(prefix="TmP") as tmp_file: - temp_dir = os.path.dirname(tmp_file.name) - dest = os.path.join(temp_dir, f"{tmp_file.name}-{'b'}") - try: - os.symlink(tmp_file.name, dest) - can = True - except (OSError, NotImplementedError): - pass # symlink is not supported - finally: - if os.path.lexists(dest): - os.remove(dest) - LOGGER.debug("symlink on filesystem does%s work", "" if can else " not") - _CAN_SYMLINK = can - return _CAN_SYMLINK - - -def fs_path_id(path: str) -> str: - return path.casefold() if fs_is_case_sensitive() else path - - -__all__ = ( - "IS_CPYTHON", - "IS_GRAALPY", - "IS_MAC_ARM64", - "IS_PYPY", - "IS_RUSTPYTHON", - "IS_WIN", - "IS_ZIPAPP", - "ROOT", - "fs_is_case_sensitive", - "fs_path_id", - "fs_supports_symlink", -) diff --git a/src/virtualenv/py.typed b/src/virtualenv/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/virtualenv/report.py b/src/virtualenv/report.py deleted file mode 100644 index 87eb56a03..000000000 --- a/src/virtualenv/report.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import annotations - -import logging -import sys - -LEVELS = { - 0: logging.CRITICAL, - 1: logging.ERROR, - 2: logging.WARNING, - 3: logging.INFO, - 4: logging.DEBUG, - 5: logging.NOTSET, -} - -MAX_LEVEL = max(LEVELS.keys()) -LOGGER = logging.getLogger() - - -def setup_report(verbosity: int, show_pid: bool = False) -> int: # noqa: FBT002 - _clean_handlers(LOGGER) - verbosity = min(verbosity, MAX_LEVEL) # pragma: no cover - level = LEVELS[verbosity] - msg_format = "%(message)s" - if level <= logging.DEBUG: - locate = "module" - msg_format = f"%(relativeCreated)d {msg_format} [%(levelname)s %({locate})s:%(lineno)d]" - if show_pid: - msg_format = f"[%(process)d] {msg_format}" - formatter = logging.Formatter(msg_format) - stream_handler = logging.StreamHandler(stream=sys.stdout) - stream_handler.setLevel(level) - LOGGER.setLevel(logging.NOTSET) - stream_handler.setFormatter(formatter) - LOGGER.addHandler(stream_handler) - level_name = logging.getLevelName(level) - LOGGER.debug("setup logging to %s", level_name) - logging.getLogger("distlib").setLevel(logging.ERROR) - return verbosity - - -def _clean_handlers(log: logging.Logger) -> None: - for log_handler in list(log.handlers): # remove handlers of libraries - log.removeHandler(log_handler) - - -__all__ = [ - "LEVELS", - "MAX_LEVEL", - "setup_report", -] diff --git a/src/virtualenv/run/__init__.py b/src/virtualenv/run/__init__.py deleted file mode 100644 index 79ebc426c..000000000 --- a/src/virtualenv/run/__init__.py +++ /dev/null @@ -1,197 +0,0 @@ -from __future__ import annotations - -import logging -import os -from functools import partial -from typing import TYPE_CHECKING - -from virtualenv.app_data import make_app_data -from virtualenv.config.cli.parser import VirtualEnvConfigParser, VirtualEnvOptions -from virtualenv.report import LEVELS, setup_report -from virtualenv.run.session import Session -from virtualenv.seed.wheels.periodic_update import manual_upgrade -from virtualenv.version import __version__ - -from .plugin.activators import ActivationSelector -from .plugin.creators import CreatorSelector -from .plugin.discovery import get_discover -from .plugin.seeders import SeederSelector - -if TYPE_CHECKING: - from collections.abc import MutableMapping - - from .plugin.base import ComponentBuilder - - -def cli_run( - args: list[str], - options: VirtualEnvOptions | None = None, - setup_logging: bool = True, # noqa: FBT002 - env: MutableMapping[str, str] | None = None, -) -> Session: - """Create a virtual environment given some command line interface arguments. - - :param args: the command line arguments - :param options: passing in a ``VirtualEnvOptions`` object allows return of the parsed options - :param setup_logging: ``True`` if setup logging handlers, ``False`` to use handlers already registered - :param env: environment variables to use - - :returns: the session object of the creation (its structure for now is experimental and might change on short - notice) - - """ - env = os.environ if env is None else env - of_session = session_via_cli(args, options, setup_logging, env) - with of_session: - of_session.run() - return of_session - - -def session_via_cli( - args: list[str], - options: VirtualEnvOptions | None = None, - setup_logging: bool = True, # noqa: FBT002 - env: MutableMapping[str, str] | None = None, -) -> Session: - """Create a virtualenv session (same as cli_run, but this does not perform the creation). Use this if you just want to query what the virtual environment would look like, but not actually create it. - - :param args: the command line arguments - :param options: passing in a ``VirtualEnvOptions`` object allows return of the parsed options - :param setup_logging: ``True`` if setup logging handlers, ``False`` to use handlers already registered - :param env: environment variables to use - - :returns: the session object of the creation (its structure for now is experimental and might change on short - notice) - - """ - env = os.environ if env is None else env - parser, elements = build_parser(args, options, setup_logging, env) - options = parser.parse_args(args) # ty: ignore[invalid-assignment] - options.py_version = parser._interpreter.version_info # noqa: SLF001 # ty: ignore[invalid-assignment, unresolved-attribute] - creator, seeder, activators = tuple( - e.create(options) # ty: ignore[invalid-argument-type] - for e in elements - ) # create types - return Session( - options.verbosity, # ty: ignore[unresolved-attribute, invalid-argument-type] - options.app_data, # ty: ignore[unresolved-attribute] - parser._interpreter, # noqa: SLF001 # ty: ignore[invalid-argument-type] - creator, # ty: ignore[invalid-argument-type] - seeder, # ty: ignore[invalid-argument-type] - activators, # ty: ignore[invalid-argument-type] - ) - - -def build_parser( - args: list[str] | None = None, - options: VirtualEnvOptions | None = None, - setup_logging: bool = True, # noqa: FBT002 - env: MutableMapping[str, str] | None = None, -) -> tuple[VirtualEnvConfigParser, list[ComponentBuilder]]: - parser = VirtualEnvConfigParser(options, os.environ if env is None else env) - add_version_flag(parser) - parser.add_argument( - "--with-traceback", - dest="with_traceback", - action="store_true", - default=False, - help="on failure also display the stacktrace internals of virtualenv", - ) - _do_report_setup(parser, args, setup_logging) - options = load_app_data(args, parser, options) - handle_extra_commands(options) - - discover = get_discover(parser, args) - parser._interpreter = interpreter = discover.interpreter # noqa: SLF001 - if interpreter is None: - msg = f"failed to find interpreter for {discover}" - raise RuntimeError(msg) - elements: list[ComponentBuilder] = [ - CreatorSelector(interpreter, parser), - SeederSelector(interpreter, parser), - ActivationSelector(interpreter, parser), - ] - options, _ = parser.parse_known_args(args) - for element in elements: - element.handle_selected_arg_parse(options) - parser.enable_help() - return parser, elements - - -def build_parser_only(args: list[str] | None = None) -> VirtualEnvConfigParser: - """Used to provide a parser for the doc generation.""" - return build_parser(args)[0] - - -def handle_extra_commands(options: VirtualEnvOptions) -> None: - if options.upgrade_embed_wheels: - result = manual_upgrade(options.app_data, options.env) - raise SystemExit(result) - - -def load_app_data( - args: list[str] | None, parser: VirtualEnvConfigParser, options: VirtualEnvOptions | None -) -> VirtualEnvOptions: - parser.add_argument( - "--read-only-app-data", - action="store_true", - help="use app data folder in read-only mode (write operations will fail with error)", - ) - options, _ = parser.parse_known_args(args, namespace=options) - - # here we need a write-able application data (e.g. the zipapp might need this for discovery cache) - parser.add_argument( - "--app-data", - help="a data folder used as cache by the virtualenv", - type=partial(make_app_data, read_only=options.read_only_app_data, env=options.env), - default=make_app_data(None, read_only=options.read_only_app_data, env=options.env), - ) - parser.add_argument( - "--reset-app-data", - action="store_true", - help="start with empty app data folder", - ) - parser.add_argument( - "--upgrade-embed-wheels", - action="store_true", - help="trigger a manual update of the embedded wheels", - ) - options, _ = parser.parse_known_args(args, namespace=options) - if options.reset_app_data: - options.app_data.reset() - return options - - -def add_version_flag(parser: VirtualEnvConfigParser) -> None: - import virtualenv # noqa: PLC0415 - - parser.add_argument( - "--version", - action="version", - version=f"%(prog)s {__version__} from {virtualenv.__file__}", - help="display the version of the virtualenv package and its location, then exit", - ) - - -def _do_report_setup(parser: VirtualEnvConfigParser, args: list[str] | None, setup_logging: bool) -> None: - level_map = ", ".join(f"{logging.getLevelName(line)}={c}" for c, line in sorted(LEVELS.items())) - msg = "verbosity = verbose - quiet, default {}, mapping => {}" - verbosity_group = parser.add_argument_group( - title="verbosity", - description=msg.format(logging.getLevelName(LEVELS[3]), level_map), - ) - verbosity = verbosity_group.add_mutually_exclusive_group() - verbosity.add_argument("-v", "--verbose", action="count", dest="verbose", help="increase verbosity", default=2) - verbosity.add_argument("-q", "--quiet", action="count", dest="quiet", help="decrease verbosity", default=0) - # do not configure logging if only help is requested, as no logging is required for this - if args and any(i in args for i in ("-h", "--help")): - return - option, _ = parser.parse_known_args(args) - if setup_logging: - setup_report(option.verbosity) # ty: ignore[invalid-argument-type] - - -__all__ = [ - "cli_run", - "session_via_cli", -] diff --git a/src/virtualenv/run/plugin/__init__.py b/src/virtualenv/run/plugin/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/virtualenv/run/plugin/activators.py b/src/virtualenv/run/plugin/activators.py deleted file mode 100644 index 6ef5df014..000000000 --- a/src/virtualenv/run/plugin/activators.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import annotations - -from argparse import ArgumentTypeError -from collections import OrderedDict -from typing import TYPE_CHECKING - -from .base import ComponentBuilder - -if TYPE_CHECKING: - from collections.abc import Sequence - - from python_discovery import PythonInfo - - from virtualenv.activation.activator import Activator - from virtualenv.config.cli.parser import VirtualEnvConfigParser, VirtualEnvOptions - - -class ActivationSelector(ComponentBuilder): - def __init__(self, interpreter: PythonInfo, parser: VirtualEnvConfigParser) -> None: - self.default = None - possible = OrderedDict( - (k, v) - for k, v in self.options("virtualenv.activate").items() - if v.supports(interpreter) # ty: ignore[unresolved-attribute] - ) - super().__init__(interpreter, parser, "activators", possible) - self.parser.description = "options for activation scripts" - self.active = None - - def add_selector_arg_parse(self, name: str, choices: Sequence[str]) -> None: - self.default = ",".join(choices) - self.parser.add_argument( - f"--{name}", - default=self.default, - metavar="comma_sep_list", - required=False, - help="activators to generate - default is all supported", - type=self._extract_activators, - ) - - def _extract_activators(self, entered_str: str) -> list[str]: - elements = [e.strip() for e in entered_str.split(",") if e.strip()] - missing = [e for e in elements if e not in self.possible] - if missing: - msg = f"the following activators are not available {','.join(missing)}" - raise ArgumentTypeError(msg) - return elements - - def handle_selected_arg_parse(self, options: VirtualEnvOptions) -> None: # ty: ignore[invalid-method-override] - selected_activators = ( - self._extract_activators(self.default) if options.activators is self.default else options.activators - ) - self.active = {k: v for k, v in self.possible.items() if k in selected_activators} - self.parser.add_argument( - "--prompt", - dest="prompt", - metavar="prompt", - help=( - "provides an alternative prompt prefix for this environment " - "(value of . means name of the current working directory)" - ), - default=None, - ) - for activator in self.active.values(): - activator.add_parser_arguments(self.parser, self.interpreter) # ty: ignore[unresolved-attribute] - - def create(self, options: VirtualEnvOptions) -> list[Activator]: - assert self.active is not None # noqa: S101 # Set by handle_selected_arg_parse - return [activator_class(options) for activator_class in self.active.values()] - - -__all__ = [ - "ActivationSelector", -] diff --git a/src/virtualenv/run/plugin/base.py b/src/virtualenv/run/plugin/base.py deleted file mode 100644 index 489517514..000000000 --- a/src/virtualenv/run/plugin/base.py +++ /dev/null @@ -1,77 +0,0 @@ -from __future__ import annotations - -import sys -from collections import OrderedDict -from importlib.metadata import entry_points -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Sequence - - from python_discovery import PythonInfo - - from virtualenv.config.cli.parser import VirtualEnvConfigParser, VirtualEnvOptions - -importlib_metadata_version = () - - -class PluginLoader: - _OPTIONS = None - _ENTRY_POINTS = None - - @classmethod - def entry_points_for(cls, key: str) -> OrderedDict[str, type]: - if sys.version_info >= (3, 10) or importlib_metadata_version >= (3, 6): - return OrderedDict((e.name, e.load()) for e in cls.entry_points().select(group=key)) # ty: ignore[unresolved-attribute] - return OrderedDict((e.name, e.load()) for e in cls.entry_points().get(key, {})) # ty: ignore[unresolved-attribute] - - @staticmethod - def entry_points() -> object: - if PluginLoader._ENTRY_POINTS is None: - PluginLoader._ENTRY_POINTS = entry_points() - return PluginLoader._ENTRY_POINTS - - -class ComponentBuilder(PluginLoader): - def __init__( - self, interpreter: PythonInfo, parser: VirtualEnvConfigParser, name: str, possible: dict[str, type] - ) -> None: - self.interpreter = interpreter - self.name = name - self._impl_class = None - self.possible = possible - self.parser = parser.add_argument_group(title=name) - self.add_selector_arg_parse(name, list(self.possible)) - - @classmethod - def options(cls, key: str) -> OrderedDict[str, type]: - if cls._OPTIONS is None: - cls._OPTIONS = cls.entry_points_for(key) - return cls._OPTIONS - - def add_selector_arg_parse(self, name: str, choices: Sequence[str]) -> None: - raise NotImplementedError - - def handle_selected_arg_parse(self, options: VirtualEnvOptions) -> str: - selected = getattr(options, self.name) - if selected not in self.possible: - msg = f"No implementation for {self.interpreter}" - raise RuntimeError(msg) - self._impl_class = self.possible[selected] - self.populate_selected_argparse(selected, options.app_data) - return selected - - def populate_selected_argparse(self, selected: str, app_data: object) -> None: - self.parser.description = f"options for {self.name} {selected}" - assert self._impl_class is not None # noqa: S101 # Set by handle_selected_arg_parse - self._impl_class.add_parser_arguments(self.parser, self.interpreter, app_data) # ty: ignore[unresolved-attribute] - - def create(self, options: VirtualEnvOptions) -> object: - assert self._impl_class is not None # noqa: S101 # Set by handle_selected_arg_parse - return self._impl_class(options, self.interpreter) - - -__all__ = [ - "ComponentBuilder", - "PluginLoader", -] diff --git a/src/virtualenv/run/plugin/creators.py b/src/virtualenv/run/plugin/creators.py deleted file mode 100644 index d1de11c2b..000000000 --- a/src/virtualenv/run/plugin/creators.py +++ /dev/null @@ -1,98 +0,0 @@ -from __future__ import annotations - -from collections import OrderedDict, defaultdict -from typing import TYPE_CHECKING, NamedTuple - -from virtualenv.create.describe import Describe -from virtualenv.create.via_global_ref.builtin.builtin_way import VirtualenvBuiltin - -from .base import ComponentBuilder - -if TYPE_CHECKING: - from collections.abc import Sequence - - from python_discovery import PythonInfo - - from virtualenv.config.cli.parser import VirtualEnvConfigParser, VirtualEnvOptions - from virtualenv.create.creator import Creator, CreatorMeta - - -class CreatorInfo(NamedTuple): - key_to_class: dict[str, type[Creator]] - key_to_meta: dict[str, CreatorMeta] - describe: type[Describe] | None - builtin_key: str - - -class CreatorSelector(ComponentBuilder): - def __init__(self, interpreter: PythonInfo, parser: VirtualEnvConfigParser) -> None: - creators, self.key_to_meta, self.describe, self.builtin_key = self.for_interpreter(interpreter) - super().__init__(interpreter, parser, "creator", creators) # ty: ignore[invalid-argument-type] - - @classmethod - def for_interpreter(cls, interpreter: PythonInfo) -> CreatorInfo: - key_to_class, key_to_meta, builtin_key, describe = OrderedDict(), {}, None, None - errors = defaultdict(list) - for key, creator_class in cls.options("virtualenv.create").items(): - if key == "builtin": - msg = "builtin creator is a reserved name" - raise RuntimeError(msg) - meta = creator_class.can_create(interpreter) # ty: ignore[unresolved-attribute] - if meta: - if meta.error: - errors[meta.error].append(creator_class) - else: - if "builtin" not in key_to_class and issubclass(creator_class, VirtualenvBuiltin): - builtin_key = key - key_to_class["builtin"] = creator_class - key_to_meta["builtin"] = meta - key_to_class[key] = creator_class - key_to_meta[key] = meta - if describe is None and issubclass(creator_class, Describe) and creator_class.can_describe(interpreter): - describe = creator_class - if not key_to_meta: - if errors: - rows = [f"{k} for creators {', '.join(i.__name__ for i in v)}" for k, v in errors.items()] - raise RuntimeError("\n".join(rows)) - msg = f"No virtualenv implementation for {interpreter}" - raise RuntimeError(msg) - return CreatorInfo( - key_to_class=key_to_class, - key_to_meta=key_to_meta, - describe=describe, - builtin_key=builtin_key or "", - ) - - def add_selector_arg_parse(self, name: str, choices: Sequence[str]) -> None: - # prefer the built-in venv if present, otherwise fallback to first defined type - choices = sorted(choices, key=lambda a: 0 if a == "builtin" else 1) - default_value = self._get_default(choices) - self.parser.add_argument( - f"--{name}", - choices=choices, - default=default_value, - required=False, - help=f"create environment via{'' if self.builtin_key is None else f' (builtin = {self.builtin_key})'}", - ) - - @staticmethod - def _get_default(choices: list[str]) -> str: - return next(iter(choices)) - - def populate_selected_argparse(self, selected: str, app_data: object) -> None: - self.parser.description = f"options for {self.name} {selected}" - assert self._impl_class is not None # noqa: S101 # Set by handle_selected_arg_parse - self._impl_class.add_parser_arguments(self.parser, self.interpreter, self.key_to_meta[selected], app_data) # ty: ignore[unresolved-attribute] - - def create(self, options: VirtualEnvOptions) -> Creator: - options.meta = self.key_to_meta[getattr(options, self.name)] - assert self._impl_class is not None # noqa: S101 # Set by handle_selected_arg_parse - if not issubclass(self._impl_class, Describe): - options.describe = self.describe(options, self.interpreter) # ty: ignore[call-non-callable, invalid-argument-type] - return super().create(options) # ty: ignore[invalid-return-type] - - -__all__ = [ - "CreatorInfo", - "CreatorSelector", -] diff --git a/src/virtualenv/run/plugin/discovery.py b/src/virtualenv/run/plugin/discovery.py deleted file mode 100644 index 837a3bcd7..000000000 --- a/src/virtualenv/run/plugin/discovery.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from .base import PluginLoader - -if TYPE_CHECKING: - from virtualenv.config.cli.parser import VirtualEnvConfigParser - from virtualenv.discovery.discover import Discover - - -class Discovery(PluginLoader): - """Discovery plugins.""" - - -def get_discover(parser: VirtualEnvConfigParser, args: list[str] | None) -> Discover: - discover_types = Discovery.entry_points_for("virtualenv.discovery") - discovery_parser = parser.add_argument_group( - title="discovery", - description="discover and provide a target interpreter", - ) - choices = _get_default_discovery(discover_types) - # prefer the builtin if present, otherwise fallback to first defined type - choices = sorted(choices, key=lambda a: 0 if a == "builtin" else 1) - try: - default_discovery = next(iter(choices)) - except StopIteration as e: - msg = "No discovery plugin found. Try reinstalling virtualenv to fix this issue." - raise RuntimeError(msg) from e - discovery_parser.add_argument( - "--discovery", - choices=choices, - default=default_discovery, - required=False, - help="interpreter discovery method", - ) - options, _ = parser.parse_known_args(args) - discovery = options.discovery - if discovery not in discover_types: - available = ", ".join(sorted(discover_types)) - msg = ( - f"discovery {discovery!r} is not available. " - f"Available discovery methods: {available}. " - f"Is the plugin installed?" - ) - raise RuntimeError(msg) - discover_class = discover_types[discovery] - discover_class.add_parser_arguments(discovery_parser) # ty: ignore[unresolved-attribute] - options, _ = parser.parse_known_args(args, namespace=options) - return discover_class(options) - - -def _get_default_discovery(discover_types: dict[str, type]) -> list[str]: - return list(discover_types.keys()) - - -__all__ = [ - "Discovery", - "get_discover", -] diff --git a/src/virtualenv/run/plugin/seeders.py b/src/virtualenv/run/plugin/seeders.py deleted file mode 100644 index 377b58e06..000000000 --- a/src/virtualenv/run/plugin/seeders.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from .base import ComponentBuilder - -if TYPE_CHECKING: - from collections.abc import Sequence - - from python_discovery import PythonInfo - - from virtualenv.config.cli.parser import VirtualEnvConfigParser, VirtualEnvOptions - from virtualenv.seed.seeder import Seeder - - -class SeederSelector(ComponentBuilder): - def __init__(self, interpreter: PythonInfo, parser: VirtualEnvConfigParser) -> None: - possible = self.options("virtualenv.seed") - super().__init__(interpreter, parser, "seeder", possible) - - def add_selector_arg_parse(self, name: str, choices: Sequence[str]) -> None: - self.parser.add_argument( - f"--{name}", - choices=choices, - default=self._get_default(), - required=False, - help="seed packages install method", - ) - self.parser.add_argument( - "--no-seed", - "--without-pip", - help="do not install seed packages", - action="store_true", - dest="no_seed", - ) - - @staticmethod - def _get_default() -> str: - return "app-data" - - def handle_selected_arg_parse(self, options: VirtualEnvOptions) -> str: - return super().handle_selected_arg_parse(options) - - def create(self, options: VirtualEnvOptions) -> Seeder: - assert self._impl_class is not None # noqa: S101 # Set by handle_selected_arg_parse - return self._impl_class(options) - - -__all__ = [ - "SeederSelector", -] diff --git a/src/virtualenv/run/session.py b/src/virtualenv/run/session.py deleted file mode 100644 index 3de6726b2..000000000 --- a/src/virtualenv/run/session.py +++ /dev/null @@ -1,121 +0,0 @@ -from __future__ import annotations - -import json -import logging -import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from types import TracebackType - - from python_discovery import PythonInfo - - from virtualenv.activation.activator import Activator - from virtualenv.app_data.base import AppData - from virtualenv.create.creator import Creator - from virtualenv.seed.seeder import Seeder - -if sys.version_info >= (3, 11): - from typing import Self -else: - from typing_extensions import Self - -LOGGER = logging.getLogger(__name__) - - -class Session: - """Represents a virtual environment creation session.""" - - def __init__( # noqa: PLR0913 - self, - verbosity: int, - app_data: AppData, - interpreter: PythonInfo, - creator: Creator, - seeder: Seeder, - activators: list[Activator], - ) -> None: - self._verbosity = verbosity - self._app_data = app_data - self._interpreter = interpreter - self._creator = creator - self._seeder = seeder - self._activators = activators - - @property - def verbosity(self) -> int: - """The verbosity of the run.""" - return self._verbosity - - @property - def interpreter(self) -> PythonInfo: - """Create a virtual environment based on this reference interpreter.""" - return self._interpreter - - @property - def creator(self) -> Creator: - """The creator used to build the virtual environment (must be compatible with the interpreter).""" - return self._creator - - @property - def seeder(self) -> Seeder: - """The mechanism used to provide the seed packages (pip, setuptools, wheel).""" - return self._seeder - - @property - def activators(self) -> list[Activator]: - """Activators used to generate activations scripts.""" - return self._activators - - def run(self) -> None: - self._create() - self._seed() - self._activate() - self.creator.pyenv_cfg.write() - - def _create(self) -> None: - LOGGER.info("create virtual environment via %s", self.creator) - self.creator.run() - LOGGER.debug(_DEBUG_MARKER) - LOGGER.debug("%s", _Debug(self.creator)) - - def _seed(self) -> None: - if self.seeder is not None and self.seeder.enabled: - LOGGER.info("add seed packages via %s", self.seeder) - self.seeder.run(self.creator) - - def _activate(self) -> None: - if self.activators: - active = ", ".join(type(i).__name__.replace("Activator", "") for i in self.activators) - LOGGER.info("add activators for %s", active) - for activator in self.activators: - activator.generate(self.creator) - - def __enter__(self) -> Self: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - self._app_data.close() - - -_DEBUG_MARKER = "=" * 30 + " target debug " + "=" * 30 - - -class _Debug: - """lazily populate debug.""" - - def __init__(self, creator: Creator) -> None: - self.creator = creator - - def __repr__(self) -> str: - return json.dumps(self.creator.debug, indent=2) - - -__all__ = [ - "Session", -] diff --git a/src/virtualenv/seed/__init__.py b/src/virtualenv/seed/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/virtualenv/seed/embed/__init__.py b/src/virtualenv/seed/embed/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/virtualenv/seed/embed/base_embed.py b/src/virtualenv/seed/embed/base_embed.py deleted file mode 100644 index 9980dab1a..000000000 --- a/src/virtualenv/seed/embed/base_embed.py +++ /dev/null @@ -1,150 +0,0 @@ -from __future__ import annotations - -import logging -from abc import ABC -from argparse import SUPPRESS -from pathlib import Path -from typing import TYPE_CHECKING - -from virtualenv.seed.seeder import Seeder -from virtualenv.seed.wheels import Version - -if TYPE_CHECKING: - from argparse import ArgumentParser - - from python_discovery import PythonInfo - - from virtualenv.app_data.base import AppData - from virtualenv.config.cli.parser import VirtualEnvOptions - -LOGGER = logging.getLogger(__name__) -PERIODIC_UPDATE_ON_BY_DEFAULT = True - - -class BaseEmbed(Seeder, ABC): - def __init__(self, options: VirtualEnvOptions) -> None: - super().__init__(options, enabled=options.no_seed is False) - - self.download = options.download - self.extra_search_dir = [i.resolve() for i in options.extra_search_dir if i.exists()] - - self.pip_version = options.pip - self.setuptools_version = options.setuptools - - # wheel version needs special handling - # on Python > 3.8, the default is None (as in not used) - # so we can differentiate between explicit and implicit none - self.wheel_version = options.wheel or "none" - - self.no_pip = options.no_pip - self.no_setuptools = options.no_setuptools - self.no_wheel = options.no_wheel - self.app_data = options.app_data - self.periodic_update = not options.no_periodic_update - - if options.py_version[:2] >= (3, 9): - if options.wheel is not None or options.no_wheel: - LOGGER.warning( - "The --no-wheel and --wheel options are deprecated. " - "They have no effect for Python > 3.8 as wheel is no longer " - "bundled in virtualenv.", - ) - self.no_wheel = True - - if not self.distribution_to_versions(): - self.enabled = False - - @classmethod - def distributions(cls) -> dict[str, str]: - return { - "pip": Version.bundle, - "setuptools": Version.bundle, - "wheel": Version.bundle, - } - - def distribution_to_versions(self) -> dict[str, str]: - return { - distribution: getattr(self, f"{distribution}_version") - for distribution in self.distributions() - if getattr(self, f"no_{distribution}", None) is False and getattr(self, f"{distribution}_version") != "none" - } - - @classmethod - def add_parser_arguments(cls, parser: ArgumentParser, interpreter: PythonInfo, app_data: AppData) -> None: # noqa: ARG003 - group = parser.add_mutually_exclusive_group() - group.add_argument( - "--no-download", - "--never-download", - dest="download", - action="store_false", - help=f"pass to disable download of the latest {'/'.join(cls.distributions())} from PyPI", - default=True, - ) - group.add_argument( - "--download", - dest="download", - action="store_true", - help=f"pass to enable download of the latest {'/'.join(cls.distributions())} from PyPI", - default=False, - ) - parser.add_argument( - "--extra-search-dir", - metavar="d", - type=Path, - nargs="+", - help="a path containing wheels to extend the internal wheel list (can be set 1+ times)", - default=[], - ) - for distribution, default in cls.distributions().items(): - help_ = f"version of {distribution} to install as seed: embed, bundle, none or exact version" - if interpreter.version_info[:2] >= (3, 12) and distribution in {"wheel", "setuptools"}: - default = "none" # noqa: PLW2901 - if interpreter.version_info[:2] >= (3, 9) and distribution == "wheel": - default = None # noqa: PLW2901 - help_ = SUPPRESS - parser.add_argument( - f"--{distribution}", - dest=distribution, - metavar="version", - help=help_, - default=default, - ) - for distribution in cls.distributions(): - help_ = f"do not install {distribution}" - if interpreter.version_info[:2] >= (3, 9) and distribution == "wheel": - help_ = SUPPRESS - parser.add_argument( - f"--no-{distribution}", - dest=f"no_{distribution}", - action="store_true", - help=help_, - default=False, - ) - parser.add_argument( - "--no-periodic-update", - dest="no_periodic_update", - action="store_true", - help="disable the periodic (once every 14 days) update of the embedded wheels", - default=not PERIODIC_UPDATE_ON_BY_DEFAULT, - ) - - def __repr__(self) -> str: - result = self.__class__.__name__ - result += "(" - if self.extra_search_dir: - result += f"extra_search_dir={', '.join(str(i) for i in self.extra_search_dir)}," - result += f"download={self.download}," - for distribution in self.distributions(): - if getattr(self, f"no_{distribution}", None): - continue - version = getattr(self, f"{distribution}_version", None) - if version == "none": - continue - ver = f"={version or 'latest'}" - result += f" {distribution}{ver}," - return result[:-1] + ")" - - -__all__ = [ - "BaseEmbed", -] diff --git a/src/virtualenv/seed/embed/pip_invoke.py b/src/virtualenv/seed/embed/pip_invoke.py deleted file mode 100644 index fb442291c..000000000 --- a/src/virtualenv/seed/embed/pip_invoke.py +++ /dev/null @@ -1,83 +0,0 @@ -from __future__ import annotations - -import logging -from contextlib import contextmanager -from subprocess import Popen -from typing import TYPE_CHECKING - -from virtualenv.seed.embed.base_embed import BaseEmbed -from virtualenv.seed.wheels import Version, get_wheel, pip_wheel_env_run -from virtualenv.util.subprocess import LogCmd - -if TYPE_CHECKING: - from collections.abc import Generator - from pathlib import Path - - from virtualenv.config.cli.parser import VirtualEnvOptions - from virtualenv.create.creator import Creator - -LOGGER = logging.getLogger(__name__) - - -class PipInvoke(BaseEmbed): - def __init__(self, options: VirtualEnvOptions) -> None: - super().__init__(options) - - def run(self, creator: Creator) -> None: - if not self.enabled: - return - for_py_version = creator.interpreter.version_release_str - with self.get_pip_install_cmd(creator.exe, for_py_version) as cmd: - env = pip_wheel_env_run(self.extra_search_dir, self.app_data, self.env) - self._execute(cmd, env) - - @staticmethod - def _execute(cmd: list[str], env: dict[str, str]) -> Popen[bytes]: - LOGGER.debug("pip seed by running: %s", LogCmd(cmd, env)) - process = Popen(cmd, env=env) - process.communicate() - if process.returncode != 0: - msg = f"failed seed with code {process.returncode}" - raise RuntimeError(msg) - return process - - @contextmanager - def get_pip_install_cmd(self, exe: Path, for_py_version: str) -> Generator[list[str], None, None]: - cmd = [ - str(exe), - "-m", - "pip", - "-q", - "install", - "--only-binary", - ":all:", - "--disable-pip-version-check", - "--ignore-installed", - ] - if not self.download: - cmd.append("--no-index") - folders = set() - for dist, version in self.distribution_to_versions().items(): - wheel = get_wheel( - distribution=dist, - version=version, - for_py_version=for_py_version, - search_dirs=self.extra_search_dir, - download=False, - app_data=self.app_data, - do_periodic_update=self.periodic_update, - env=self.env, - ) - if wheel is None: - msg = f"could not get wheel for distribution {dist}" - raise RuntimeError(msg) - folders.add(str(wheel.path.parent)) - cmd.append(Version.as_pip_req(dist, wheel.version)) - for folder in sorted(folders): - cmd.extend(["--find-links", str(folder)]) - yield cmd - - -__all__ = [ - "PipInvoke", -] diff --git a/src/virtualenv/seed/embed/via_app_data/__init__.py b/src/virtualenv/seed/embed/via_app_data/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/virtualenv/seed/embed/via_app_data/pip_install/__init__.py b/src/virtualenv/seed/embed/via_app_data/pip_install/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/virtualenv/seed/embed/via_app_data/pip_install/base.py b/src/virtualenv/seed/embed/via_app_data/pip_install/base.py deleted file mode 100644 index 89348aaa0..000000000 --- a/src/virtualenv/seed/embed/via_app_data/pip_install/base.py +++ /dev/null @@ -1,214 +0,0 @@ -from __future__ import annotations - -import logging -import os -import re -import zipfile -from abc import ABC, abstractmethod -from configparser import ConfigParser -from itertools import chain -from pathlib import Path -from tempfile import mkdtemp -from typing import TYPE_CHECKING - -from distlib.scripts import ScriptMaker, enquote_executable - -from virtualenv.util.path import safe_delete - -if TYPE_CHECKING: - from virtualenv.create.creator import Creator - -LOGGER = logging.getLogger(__name__) - - -class PipInstall(ABC): - def __init__(self, wheel: Path, creator: Creator, image_folder: Path) -> None: - self._wheel = wheel - self._creator = creator - self._image_dir = image_folder - self._extracted = False - self.__dist_info = None - self._console_entry_points = None - - @abstractmethod - def _sync(self, src: Path, dst: Path) -> None: - raise NotImplementedError - - def install(self, version_info: tuple[int, ...]) -> None: - self._extracted = True - self._uninstall_previous_version() - # sync image - for filename in self._image_dir.iterdir(): - into = self._creator.purelib / filename.name - self._sync(filename, into) - # generate console executables - consoles = set() - script_dir = self._creator.script_dir - for name, module in self._console_scripts.items(): # ty: ignore[unresolved-attribute] - consoles.update(self._create_console_entry_point(name, module, script_dir, version_info)) - LOGGER.debug("generated console scripts %s", " ".join(i.name for i in consoles)) - - def build_image(self) -> None: - # 1. first extract the wheel - LOGGER.debug("build install image for %s to %s", self._wheel.name, self._image_dir) - with zipfile.ZipFile(str(self._wheel)) as zip_ref: - self._shorten_path_if_needed(zip_ref) - zip_ref.extractall(str(self._image_dir)) - self._extracted = True - # 2. now add additional files not present in the distribution - new_files = self._generate_new_files() - # 3. finally fix the records file - self._fix_records(new_files) - - def _shorten_path_if_needed(self, zip_ref: zipfile.ZipFile) -> None: - if os.name == "nt": - to_folder = str(self._image_dir) - # https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation - zip_max_len = max(len(i) for i in zip_ref.namelist()) - path_len = zip_max_len + len(to_folder) - if path_len > 260: # noqa: PLR2004 - self._image_dir.mkdir(exist_ok=True) # to get a short path must exist - - from virtualenv.util.path import get_short_path_name # noqa: PLC0415 - - to_folder = get_short_path_name(to_folder) - self._image_dir = Path(to_folder) - - def _records_text(self, files: set[Path] | list[Path]) -> str: - return "\n".join(f"{os.path.relpath(str(rec), str(self._image_dir))},," for rec in files) - - def _generate_new_files(self) -> set[Path]: - new_files = set() - installer = self._dist_info / "INSTALLER" # ty: ignore[unsupported-operator] - installer.write_text("pip\n", encoding="utf-8") - new_files.add(installer) - # inject a no-op root element, as workaround for bug in https://github.com/pypa/pip/issues/7226 - marker = self._image_dir / f"{self._dist_info.stem}.virtualenv" # ty: ignore[unresolved-attribute] - marker.write_text("", encoding="utf-8") - new_files.add(marker) - folder = mkdtemp() - try: - to_folder = Path(folder) - rel = os.path.relpath(str(self._creator.script_dir), str(self._creator.purelib)) - version_info = self._creator.interpreter.version_info - for name, module in self._console_scripts.items(): # ty: ignore[unresolved-attribute] - new_files.update( - Path(os.path.normpath(str(self._image_dir / rel / i.name))) - for i in self._create_console_entry_point(name, module, to_folder, version_info) # ty: ignore[invalid-argument-type] - ) - finally: - safe_delete(folder) # ty: ignore[invalid-argument-type] - return new_files - - @property - def _dist_info(self) -> Path | None: - if self._extracted is False: - return None # pragma: no cover - if self.__dist_info is None: - files = [] - for filename in self._image_dir.iterdir(): - files.append(filename.name) - if filename.suffix == ".dist-info": - self.__dist_info = filename - break - else: - msg = f"no .dist-info at {self._image_dir}, has {', '.join(files)}" - raise RuntimeError(msg) # pragma: no cover - return self.__dist_info - - @abstractmethod - def _fix_records(self, extra_record_data: set[Path]) -> None: - raise NotImplementedError - - @property - def _console_scripts(self) -> dict[str, str] | None: - if self._extracted is False: - return None # pragma: no cover - if self._console_entry_points is None: - self._console_entry_points = {} - entry_points = self._dist_info / "entry_points.txt" # ty: ignore[unsupported-operator] - if entry_points.exists(): - parser = ConfigParser() - with entry_points.open(encoding="utf-8") as file_handler: - parser.read_file(file_handler) - if "console_scripts" in parser.sections(): - for name, value in parser.items("console_scripts"): - match = re.match(r"(.*?)-?\d\.?\d*", name) - our_name = match.groups(1)[0] if match else name - self._console_entry_points[our_name] = value - return self._console_entry_points - - def _create_console_entry_point( - self, name: str, value: str, to_folder: Path, version_info: tuple[int, ...] - ) -> list[Path]: - result = [] - maker = ScriptMakerCustom(to_folder, version_info, self._creator.exe, name) - specification = f"{name} = {value}" - new_files = maker.make(specification) - result.extend(Path(i) for i in new_files) - return result - - def _uninstall_previous_version(self) -> None: - dist_name = self._dist_info.stem.split("-")[0] # ty: ignore[unresolved-attribute] - in_folders = chain.from_iterable([i.iterdir() for i in (self._creator.purelib, self._creator.platlib)]) - paths = (p for p in in_folders if p.stem.split("-")[0] == dist_name and p.suffix == ".dist-info" and p.is_dir()) - existing_dist = next(paths, None) - if existing_dist is not None: - self._uninstall_dist(existing_dist) - - @staticmethod - def _uninstall_dist(dist: Path) -> None: - dist_base = dist.parent - LOGGER.debug("uninstall existing distribution %s from %s", dist.stem, dist_base) - - top_txt = dist / "top_level.txt" # add top level packages at folder level - paths = ( - {dist.parent / i.strip() for i in top_txt.read_text(encoding="utf-8").splitlines()} - if top_txt.exists() - else set() - ) - paths.add(dist) # add the dist-info folder itself - - base_dirs, record = paths.copy(), dist / "RECORD" # collect entries in record that we did not register yet - for name in ( - (i.split(",")[0] for i in record.read_text(encoding="utf-8").splitlines()) if record.exists() else () - ): - path = dist_base / name - if not any(p in base_dirs for p in path.parents): # only add if not already added as a base dir - paths.add(path) - - for path in sorted(paths): # actually remove stuff in a stable order - if path.exists(): - if path.is_dir() and not path.is_symlink(): - safe_delete(path) - else: - path.unlink() - - def clear(self) -> None: - if self._image_dir.exists(): - safe_delete(self._image_dir) - - def has_image(self) -> bool: - return self._image_dir.exists() and any(self._image_dir.iterdir()) - - -class ScriptMakerCustom(ScriptMaker): - def __init__(self, target_dir: Path, version_info: tuple[int, ...], executable: Path, name: str) -> None: - super().__init__(None, str(target_dir)) - self.clobber = True # overwrite - self.set_mode = True # ensure they are executable - self.executable = enquote_executable(str(executable)) - self.version_info = version_info.major, version_info.minor # ty: ignore[unresolved-attribute] - self.variants = {"", "X", "X.Y"} - self._name = name - - def _write_script( - self, names: set[str], shebang: bytes, script_bytes: bytes, filenames: list[str], ext: str - ) -> None: - names.add(f"{self._name}{self.version_info[0]}.{self.version_info[1]}") - super()._write_script(names, shebang, script_bytes, filenames, ext) - - -__all__ = [ - "PipInstall", -] diff --git a/src/virtualenv/seed/embed/via_app_data/pip_install/copy.py b/src/virtualenv/seed/embed/via_app_data/pip_install/copy.py deleted file mode 100644 index e218bf0b1..000000000 --- a/src/virtualenv/seed/embed/via_app_data/pip_install/copy.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import annotations - -import os -from pathlib import Path -from typing import TYPE_CHECKING - -from virtualenv.util.path import copy - -from .base import PipInstall - -if TYPE_CHECKING: - from collections.abc import Generator - - -class CopyPipInstall(PipInstall): - def _sync(self, src: Path, dst: Path) -> None: - copy(src, dst) - - def _generate_new_files(self) -> set[Path]: - # create the pyc files - new_files = super()._generate_new_files() - new_files.update(self._cache_files()) - return new_files - - def _cache_files(self) -> Generator[Path, None, None]: - version = self._creator.interpreter.version_info - py_c_ext = f".{self._creator.interpreter.implementation.lower()}-{version.major}{version.minor}.pyc" - for root, dirs, files in os.walk(str(self._image_dir), topdown=True): - root_path = Path(root) - for name in files: - if name.endswith(".py"): - yield root_path / f"{name[:-3]}{py_c_ext}" - for name in dirs: - yield root_path / name / "__pycache__" - - def _fix_records(self, extra_record_data: set[Path]) -> None: - extra_record_data_str = self._records_text(extra_record_data) - with (self._dist_info / "RECORD").open("ab") as file_handler: # ty: ignore[unsupported-operator] - file_handler.write(extra_record_data_str.encode("utf-8")) - - -__all__ = [ - "CopyPipInstall", -] diff --git a/src/virtualenv/seed/embed/via_app_data/pip_install/symlink.py b/src/virtualenv/seed/embed/via_app_data/pip_install/symlink.py deleted file mode 100644 index 855285673..000000000 --- a/src/virtualenv/seed/embed/via_app_data/pip_install/symlink.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import annotations - -import os -from stat import S_IREAD, S_IRGRP, S_IROTH -from subprocess import PIPE, Popen -from typing import TYPE_CHECKING - -from virtualenv.util.path import safe_delete, set_tree - -from .base import PipInstall - -if TYPE_CHECKING: - from pathlib import Path - - -class SymlinkPipInstall(PipInstall): - def _sync(self, src: Path, dst: Path) -> None: - os.symlink(str(src), str(dst)) - - def _generate_new_files(self) -> set[Path]: - # create the pyc files, as the build image will be R/O - cmd = [str(self._creator.exe), "-m", "compileall", str(self._image_dir)] - process = Popen(cmd, stdout=PIPE, stderr=PIPE) - process.communicate() - # the root pyc is shared, so we'll not symlink that - but still add the pyc files to the RECORD for close - root_py_cache = self._image_dir / "__pycache__" - new_files = set() - if root_py_cache.exists(): - new_files.update(root_py_cache.iterdir()) - new_files.add(root_py_cache) - safe_delete(root_py_cache) - core_new_files = super()._generate_new_files() - # remove files that are within the image folder deeper than one level (as these will be not linked directly) - for file in core_new_files: - try: - rel = file.relative_to(self._image_dir) - if len(rel.parts) > 1: - continue - except ValueError: - pass - new_files.add(file) - return new_files - - def _fix_records(self, extra_record_data: set[Path]) -> None: - extra_record_data.update(i for i in self._image_dir.iterdir()) - extra_record_data_str = self._records_text(sorted(extra_record_data, key=str)) # ty: ignore[invalid-argument-type] - (self._dist_info / "RECORD").write_text(extra_record_data_str, encoding="utf-8") # ty: ignore[unsupported-operator] - - def build_image(self) -> None: - super().build_image() - # protect the image by making it read only - set_tree(self._image_dir, S_IREAD | S_IRGRP | S_IROTH) - - def clear(self) -> None: - if self._image_dir.exists(): - safe_delete(self._image_dir) - super().clear() - - -__all__ = [ - "SymlinkPipInstall", -] diff --git a/src/virtualenv/seed/embed/via_app_data/via_app_data.py b/src/virtualenv/seed/embed/via_app_data/via_app_data.py deleted file mode 100644 index 092ef74bc..000000000 --- a/src/virtualenv/seed/embed/via_app_data/via_app_data.py +++ /dev/null @@ -1,160 +0,0 @@ -"""Bootstrap.""" - -from __future__ import annotations - -import logging -import sys -import traceback -from contextlib import contextmanager -from pathlib import Path -from subprocess import CalledProcessError -from threading import Lock, Thread -from typing import TYPE_CHECKING - -from virtualenv.info import fs_supports_symlink -from virtualenv.seed.embed.base_embed import BaseEmbed -from virtualenv.seed.wheels import get_wheel - -from .pip_install.copy import CopyPipInstall -from .pip_install.symlink import SymlinkPipInstall - -if TYPE_CHECKING: - from argparse import ArgumentParser - from collections.abc import Generator - - from python_discovery import PythonInfo - - from virtualenv.app_data.base import AppData - from virtualenv.config.cli.parser import VirtualEnvOptions - from virtualenv.create.creator import Creator - from virtualenv.seed.wheels.util import Wheel - - from .pip_install.base import PipInstall - -LOGGER = logging.getLogger(__name__) - - -class FromAppData(BaseEmbed): - def __init__(self, options: VirtualEnvOptions) -> None: - super().__init__(options) - self.symlinks = options.symlink_app_data - - @classmethod - def add_parser_arguments(cls, parser: ArgumentParser, interpreter: PythonInfo, app_data: AppData) -> None: - super().add_parser_arguments(parser, interpreter, app_data) - can_symlink = app_data.transient is False and fs_supports_symlink() - sym = "" if can_symlink else "not supported - " - parser.add_argument( - "--symlink-app-data", - dest="symlink_app_data", - action="store_true" if can_symlink else "store_false", - help=f"{sym} symlink the python packages from the app-data folder (requires seed pip>=19.3)", - default=False, - ) - - def run(self, creator: Creator) -> None: - if not self.enabled: - return - with self._get_seed_wheels(creator) as name_to_whl: - pip_version = name_to_whl["pip"].version_tuple if "pip" in name_to_whl else None - installer_class = self.installer_class(pip_version) - exceptions = {} - - def _install(name: str, wheel: Wheel) -> None: - try: - LOGGER.debug("install %s from wheel %s via %s", name, wheel, installer_class.__name__) - key = Path(installer_class.__name__) / wheel.path.stem - wheel_img = self.app_data.wheel_image(creator.interpreter.version_release_str, key) - installer = installer_class(wheel.path, creator, wheel_img) - parent = self.app_data.lock / wheel_img.parent - with parent.non_reentrant_lock_for_key(wheel_img.name): - if not installer.has_image(): - installer.build_image() - installer.install(creator.interpreter.version_info) # ty: ignore[invalid-argument-type] - except Exception: # noqa: BLE001 - exceptions[name] = sys.exc_info() - - threads = [Thread(target=_install, args=(n, w)) for n, w in name_to_whl.items()] - for thread in threads: - thread.start() - for thread in threads: - thread.join() - if exceptions: - messages = [f"failed to build image {', '.join(exceptions.keys())} because:"] - for value in exceptions.values(): - exc_type, exc_value, exc_traceback = value - messages.append("".join(traceback.format_exception(exc_type, exc_value, exc_traceback))) - raise RuntimeError("\n".join(messages)) - - @contextmanager - def _get_seed_wheels(self, creator: Creator) -> Generator[dict[str, Wheel], None, None]: # noqa: C901 - name_to_whl, lock, fail = {}, Lock(), {} - - def _get(distribution: str, version: str | None) -> None: - for_py_version = creator.interpreter.version_release_str - failure, result = None, None - # fallback to download in case the exact version is not available - for download in [True] if self.download else [False, True]: - failure = None - try: - result = get_wheel( - distribution=distribution, - version=version, - for_py_version=for_py_version, - search_dirs=self.extra_search_dir, - download=download, - app_data=self.app_data, - do_periodic_update=self.periodic_update, - env=self.env, - ) - if result is not None: - break - except Exception as exception: - LOGGER.exception("fail") - failure = exception - if failure: - if isinstance(failure, CalledProcessError): - msg = f"failed to download {distribution}" - if version is not None: - msg += f" version {version}" - msg += f", pip download exit code {failure.returncode}" - output = failure.output + failure.stderr - if output: - msg += "\n" - msg += output - else: - msg = repr(failure) - LOGGER.error(msg) - with lock: - fail[distribution] = version - else: - with lock: - name_to_whl[distribution] = result - - threads = [ - Thread(target=_get, args=(distribution, version)) - for distribution, version in self.distribution_to_versions().items() - ] - for thread in threads: - thread.start() - for thread in threads: - thread.join() - if fail: - msg = f"seed failed due to failing to download wheels {', '.join(fail.keys())}" - raise RuntimeError(msg) - yield name_to_whl - - def installer_class(self, pip_version_tuple: tuple[int, ...] | None) -> type[PipInstall]: - if self.symlinks and pip_version_tuple and pip_version_tuple >= (19, 3): # symlink support requires pip 19.3+ - return SymlinkPipInstall - return CopyPipInstall - - def __repr__(self) -> str: - msg = f", via={'symlink' if self.symlinks else 'copy'}, app_data_dir={self.app_data}" - base = super().__repr__() - return f"{base[:-1]}{msg}{base[-1]}" - - -__all__ = [ - "FromAppData", -] diff --git a/src/virtualenv/seed/seeder.py b/src/virtualenv/seed/seeder.py deleted file mode 100644 index 4ae6276be..000000000 --- a/src/virtualenv/seed/seeder.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from argparse import ArgumentParser - - from python_discovery import PythonInfo - - from virtualenv.app_data.base import AppData - from virtualenv.config.cli.parser import VirtualEnvOptions - from virtualenv.create.creator import Creator - - -class Seeder(ABC): - """A seeder will install some seed packages into a virtual environment.""" - - def __init__(self, options: VirtualEnvOptions, enabled: bool) -> None: - """Create. - - :param options: the parsed options as defined within :meth:`add_parser_arguments` - :param enabled: a flag weather the seeder is enabled or not - - """ - self.enabled = enabled - self.env = options.env - - @classmethod - def add_parser_arguments(cls, parser: ArgumentParser, interpreter: PythonInfo, app_data: AppData) -> None: - """Add CLI arguments for this seed mechanisms. - - :param parser: the CLI parser - :param app_data: the CLI parser - :param interpreter: the interpreter this virtual environment is based of - - """ - raise NotImplementedError - - @abstractmethod - def run(self, creator: Creator) -> None: - """Perform the seed operation. - - :param creator: the creator (based of :class:`virtualenv.create.creator.Creator`) we used to create this virtual - environment - - """ - raise NotImplementedError - - -__all__ = [ - "Seeder", -] diff --git a/src/virtualenv/seed/wheels/__init__.py b/src/virtualenv/seed/wheels/__init__.py deleted file mode 100644 index 20a398a30..000000000 --- a/src/virtualenv/seed/wheels/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import annotations - -from .acquire import get_wheel, pip_wheel_env_run -from .util import Version, Wheel - -__all__ = [ - "Version", - "Wheel", - "get_wheel", - "pip_wheel_env_run", -] diff --git a/src/virtualenv/seed/wheels/acquire.py b/src/virtualenv/seed/wheels/acquire.py deleted file mode 100644 index b6a6533ad..000000000 --- a/src/virtualenv/seed/wheels/acquire.py +++ /dev/null @@ -1,150 +0,0 @@ -"""Bootstrap.""" - -from __future__ import annotations - -import logging -import sys -from operator import eq, lt -from pathlib import Path -from subprocess import PIPE, CalledProcessError, Popen -from typing import TYPE_CHECKING - -from .bundle import from_bundle -from .periodic_update import add_wheel_to_update_log -from .util import Version, Wheel, discover_wheels - -if TYPE_CHECKING: - from virtualenv.app_data.base import AppData - -LOGGER = logging.getLogger(__name__) - - -def get_wheel( # noqa: PLR0913 - distribution: str, - version: str | None, - for_py_version: str, - search_dirs: list[Path], - download: bool, - app_data: AppData, - do_periodic_update: bool, - env: dict[str, str], -) -> Wheel | None: - """Get a wheel with the given distribution-version-for_py_version trio, by using the extra search dir + download.""" - # not all wheels are compatible with all python versions, so we need to py version qualify it - wheel = None - - if not download or version != Version.bundle: - # 1. acquire from bundle - wheel = from_bundle(distribution, version, for_py_version, search_dirs, app_data, do_periodic_update, env) - - if download and wheel is None and version != Version.embed: - # 2. download from the internet - wheel = download_wheel( - distribution=distribution, - version_spec=Version.as_version_spec(version), - for_py_version=for_py_version, - search_dirs=search_dirs, - app_data=app_data, - to_folder=app_data.house, - env=env, - ) - if wheel is not None and app_data.can_update: - add_wheel_to_update_log(wheel, for_py_version, app_data) - - return wheel - - -def download_wheel( # noqa: PLR0913 - distribution: str, - version_spec: str | None, - for_py_version: str, - search_dirs: list[Path], - app_data: AppData, - to_folder: Path, - env: dict[str, str], -) -> Wheel: - to_download = f"{distribution}{version_spec or ''}" - LOGGER.debug("download wheel %s %s to %s", to_download, for_py_version, to_folder) - cmd = [ - sys.executable, - "-m", - "pip", - "download", - "--progress-bar", - "off", - "--disable-pip-version-check", - "--only-binary=:all:", - "--no-deps", - "--python-version", - for_py_version, - "-d", - str(to_folder), - to_download, - ] - # pip has no interface in python - must be a new sub-process - env = pip_wheel_env_run(search_dirs, app_data, env) - process = Popen(cmd, env=env, stdout=PIPE, stderr=PIPE, universal_newlines=True, encoding="utf-8") - out, err = process.communicate() - if process.returncode != 0: - kwargs = {"output": out, "stderr": err} - raise CalledProcessError(process.returncode, cmd, **kwargs) - result = _find_downloaded_wheel(distribution, version_spec, for_py_version, to_folder, out) - LOGGER.debug("downloaded wheel %s", result.name) # ty: ignore[unresolved-attribute] - return result # ty: ignore[invalid-return-type] - - -def _find_downloaded_wheel( - distribution: str, version_spec: str | None, for_py_version: str, to_folder: Path, out: str -) -> Wheel | None: - for line in out.splitlines(): - stripped_line = line.lstrip() - for marker in ("Saved ", "File was already downloaded "): - if stripped_line.startswith(marker): - return Wheel(Path(stripped_line[len(marker) :]).absolute()) - # if for some reason the output does not match fallback to the latest version with that spec - return find_compatible_in_house(distribution, version_spec, for_py_version, to_folder) - - -def find_compatible_in_house( - distribution: str, version_spec: str | None, for_py_version: str, in_folder: Path -) -> Wheel | None: - wheels = discover_wheels(in_folder, distribution, None, for_py_version) - start, end = 0, len(wheels) - if version_spec is not None and version_spec: - if version_spec.startswith("<"): - from_pos, op = 1, lt - elif version_spec.startswith("=="): - from_pos, op = 2, eq - else: - raise ValueError(version_spec) - version = Wheel.as_version_tuple(version_spec[from_pos:]) - start = next((at for at, w in enumerate(wheels) if op(w.version_tuple, version)), len(wheels)) - - return None if start == end else wheels[start] - - -def pip_wheel_env_run(search_dirs: list[Path], app_data: AppData, env: dict[str, str]) -> dict[str, str]: - env = env.copy() - env.update({"PIP_USE_WHEEL": "1", "PIP_USER": "0", "PIP_NO_INPUT": "1", "PYTHONIOENCODING": "utf-8"}) - wheel = get_wheel( - distribution="pip", - version=None, - for_py_version=f"{sys.version_info.major}.{sys.version_info.minor}", - search_dirs=search_dirs, - download=False, - app_data=app_data, - do_periodic_update=False, - env=env, - ) - if wheel is None: - msg = "could not find the embedded pip" - raise RuntimeError(msg) - env["PYTHONPATH"] = str(wheel.path) - return env - - -__all__ = [ - "download_wheel", - "get_wheel", - "pip_wheel_env_run", -] diff --git a/src/virtualenv/seed/wheels/bundle.py b/src/virtualenv/seed/wheels/bundle.py deleted file mode 100644 index 0a6580a9b..000000000 --- a/src/virtualenv/seed/wheels/bundle.py +++ /dev/null @@ -1,65 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from virtualenv.seed.wheels.embed import get_embed_wheel - -from .periodic_update import periodic_update -from .util import Version, Wheel, discover_wheels - -if TYPE_CHECKING: - from pathlib import Path - - from virtualenv.app_data.base import AppData - - -def from_bundle( # noqa: PLR0913 - distribution: str, - version: str | None, - for_py_version: str, - search_dirs: list[Path], - app_data: AppData, - do_periodic_update: bool, - env: dict[str, str], -) -> Wheel | None: - """Load the bundled wheel to a cache directory.""" - of_version = Version.of_version(version) - wheel = load_embed_wheel(app_data, distribution, for_py_version, of_version) - - if version != Version.embed: - # 2. check if we have upgraded embed - if app_data.can_update: - per = do_periodic_update - wheel = periodic_update(distribution, of_version, for_py_version, wheel, search_dirs, app_data, per, env) - - # 3. acquire from extra search dir - found_wheel = from_dir(distribution, of_version, for_py_version, search_dirs) - if found_wheel is not None and (wheel is None or found_wheel.version_tuple > wheel.version_tuple): - wheel = found_wheel - return wheel - - -def load_embed_wheel(app_data: AppData, distribution: str, for_py_version: str, version: str | None) -> Wheel | None: - wheel = get_embed_wheel(distribution, for_py_version) - if wheel is not None: - version_match = version == wheel.version - if version is None or version_match: - with app_data.ensure_extracted(wheel.path, lambda: app_data.house) as wheel_path: # ty: ignore[invalid-argument-type] - wheel = Wheel(wheel_path) - else: # if version does not match ignore - wheel = None - return wheel - - -def from_dir(distribution: str, version: str | None, for_py_version: str, directories: list[Path]) -> Wheel | None: - """Load a compatible wheel from a given folder.""" - for folder in directories: - for wheel in discover_wheels(folder, distribution, version, for_py_version): - return wheel - return None - - -__all__ = [ - "from_bundle", - "load_embed_wheel", -] diff --git a/src/virtualenv/seed/wheels/embed/__init__.py b/src/virtualenv/seed/wheels/embed/__init__.py deleted file mode 100644 index c416fe0ef..000000000 --- a/src/virtualenv/seed/wheels/embed/__init__.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -from virtualenv.seed.wheels.util import Wheel - -BUNDLE_FOLDER = Path(__file__).absolute().parent -BUNDLE_SUPPORT = { - "3.8": { - "pip": "pip-25.0.1-py3-none-any.whl", - "setuptools": "setuptools-75.3.4-py3-none-any.whl", - "wheel": "wheel-0.45.1-py3-none-any.whl", - }, - "3.9": { - "pip": "pip-26.0.1-py3-none-any.whl", - "setuptools": "setuptools-82.0.1-py3-none-any.whl", - }, - "3.10": { - "pip": "pip-26.0.1-py3-none-any.whl", - "setuptools": "setuptools-82.0.1-py3-none-any.whl", - }, - "3.11": { - "pip": "pip-26.0.1-py3-none-any.whl", - "setuptools": "setuptools-82.0.1-py3-none-any.whl", - }, - "3.12": { - "pip": "pip-26.0.1-py3-none-any.whl", - "setuptools": "setuptools-82.0.1-py3-none-any.whl", - }, - "3.13": { - "pip": "pip-26.0.1-py3-none-any.whl", - "setuptools": "setuptools-82.0.1-py3-none-any.whl", - }, - "3.14": { - "pip": "pip-26.0.1-py3-none-any.whl", - "setuptools": "setuptools-82.0.1-py3-none-any.whl", - }, - "3.15": { - "pip": "pip-26.0.1-py3-none-any.whl", - "setuptools": "setuptools-82.0.1-py3-none-any.whl", - }, -} -MAX = "3.8" - - -def get_embed_wheel(distribution: str, for_py_version: str) -> Wheel | None: - mapping = BUNDLE_SUPPORT.get(for_py_version, {}) or BUNDLE_SUPPORT[MAX] - wheel_file = mapping.get(distribution) - if wheel_file is None: - return None - path = BUNDLE_FOLDER / wheel_file - return Wheel.from_path(path) - - -__all__ = [ - "BUNDLE_FOLDER", - "BUNDLE_SUPPORT", - "MAX", - "get_embed_wheel", -] diff --git a/src/virtualenv/seed/wheels/embed/pip-25.0.1-py3-none-any.whl b/src/virtualenv/seed/wheels/embed/pip-25.0.1-py3-none-any.whl deleted file mode 100644 index 8d3b0043e..000000000 Binary files a/src/virtualenv/seed/wheels/embed/pip-25.0.1-py3-none-any.whl and /dev/null differ diff --git a/src/virtualenv/seed/wheels/embed/pip-26.0.1-py3-none-any.whl b/src/virtualenv/seed/wheels/embed/pip-26.0.1-py3-none-any.whl deleted file mode 100644 index 580d09a92..000000000 Binary files a/src/virtualenv/seed/wheels/embed/pip-26.0.1-py3-none-any.whl and /dev/null differ diff --git a/src/virtualenv/seed/wheels/embed/setuptools-75.3.4-py3-none-any.whl b/src/virtualenv/seed/wheels/embed/setuptools-75.3.4-py3-none-any.whl deleted file mode 100644 index 4cacd34c2..000000000 Binary files a/src/virtualenv/seed/wheels/embed/setuptools-75.3.4-py3-none-any.whl and /dev/null differ diff --git a/src/virtualenv/seed/wheels/embed/setuptools-82.0.1-py3-none-any.whl b/src/virtualenv/seed/wheels/embed/setuptools-82.0.1-py3-none-any.whl deleted file mode 100644 index 2db749a77..000000000 Binary files a/src/virtualenv/seed/wheels/embed/setuptools-82.0.1-py3-none-any.whl and /dev/null differ diff --git a/src/virtualenv/seed/wheels/embed/wheel-0.45.1-py3-none-any.whl b/src/virtualenv/seed/wheels/embed/wheel-0.45.1-py3-none-any.whl deleted file mode 100644 index 589308a21..000000000 Binary files a/src/virtualenv/seed/wheels/embed/wheel-0.45.1-py3-none-any.whl and /dev/null differ diff --git a/src/virtualenv/seed/wheels/periodic_update.py b/src/virtualenv/seed/wheels/periodic_update.py deleted file mode 100644 index 57193ee7c..000000000 --- a/src/virtualenv/seed/wheels/periodic_update.py +++ /dev/null @@ -1,458 +0,0 @@ -"""Periodically update bundled versions.""" - -from __future__ import annotations - -import json -import logging -import os -import ssl -import sys -from datetime import datetime, timedelta, timezone -from itertools import groupby -from pathlib import Path -from shutil import copy2 -from subprocess import DEVNULL, Popen -from textwrap import dedent -from threading import Thread -from typing import TYPE_CHECKING -from urllib.error import URLError -from urllib.request import urlopen - -from virtualenv.app_data import AppDataDiskFolder -from virtualenv.seed.wheels.embed import BUNDLE_SUPPORT -from virtualenv.seed.wheels.util import Wheel -from virtualenv.util.subprocess import CREATE_NO_WINDOW - -if TYPE_CHECKING: - from collections.abc import Generator - - from virtualenv.app_data.base import AppData - -LOGGER = logging.getLogger(__name__) -GRACE_PERIOD_CI = timedelta(hours=1) # prevent version switch in the middle of a CI run -GRACE_PERIOD_MINOR = timedelta(days=28) -UPDATE_PERIOD = timedelta(days=14) -UPDATE_ABORTED_DELAY = timedelta(hours=1) - - -def periodic_update( # noqa: PLR0913 - distribution: str, - of_version: str | None, - for_py_version: str, - wheel: Wheel | None, - search_dirs: list[Path], - app_data: AppData, - do_periodic_update: bool, - env: dict[str, str], -) -> Wheel | None: - if do_periodic_update: - handle_auto_update(distribution, for_py_version, wheel, search_dirs, app_data, env) - - now = datetime.now(tz=timezone.utc) - - def _update_wheel(ver: NewVersion) -> Wheel: - updated_wheel = Wheel(app_data.house / ver.filename) - LOGGER.debug("using %supdated wheel %s", "periodically " if updated_wheel else "", updated_wheel) - return updated_wheel - - u_log = UpdateLog.from_app_data(app_data, distribution, for_py_version) - if of_version is None: - for _, group in groupby(u_log.versions, key=lambda v: v.wheel.version_tuple[0:2]): - # use only latest patch version per minor, earlier assumed to be buggy - all_patches = list(group) - ignore_grace_period_minor = any(version for version in all_patches if version.use(now)) - for version in all_patches: - if wheel is not None and Path(version.filename).name == wheel.name: - return wheel - if version.use(now, ignore_grace_period_minor): - return _update_wheel(version) - else: - for version in u_log.versions: - if version.wheel.version == of_version: - return _update_wheel(version) - - return wheel - - -def handle_auto_update( # noqa: PLR0913 - distribution: str, - for_py_version: str, - wheel: Wheel | None, - search_dirs: list[Path], - app_data: AppData, - env: dict[str, str], -) -> None: - embed_update_log = app_data.embed_update_log(distribution, for_py_version) - u_log = UpdateLog.from_dict(embed_update_log.read()) - if u_log.needs_update: - u_log.periodic = True - u_log.started = datetime.now(tz=timezone.utc) - embed_update_log.write(u_log.to_dict()) - trigger_update(distribution, for_py_version, wheel, search_dirs, app_data, periodic=True, env=env) - - -def add_wheel_to_update_log(wheel: Wheel, for_py_version: str, app_data: AppData) -> None: - embed_update_log = app_data.embed_update_log(wheel.distribution, for_py_version) - LOGGER.debug("adding %s information to %s", wheel.name, embed_update_log.file) # ty: ignore[unresolved-attribute] - u_log = UpdateLog.from_dict(embed_update_log.read()) - if any(version.filename == wheel.name for version in u_log.versions): - LOGGER.warning("%s already present in %s", wheel.name, embed_update_log.file) # ty: ignore[unresolved-attribute] - return - # we don't need a release date for sources other than "periodic" - version = NewVersion(wheel.name, datetime.now(tz=timezone.utc), None, "download") - u_log.versions.append(version) # always write at the end for proper updates - embed_update_log.write(u_log.to_dict()) - - -DATETIME_FMT = "%Y-%m-%dT%H:%M:%S.%fZ" - - -def dump_datetime(value: datetime | None) -> str | None: - return None if value is None else value.strftime(DATETIME_FMT) - - -def load_datetime(value: str | None) -> datetime | None: - return None if value is None else datetime.strptime(value, DATETIME_FMT).replace(tzinfo=timezone.utc) - - -class NewVersion: # noqa: PLW1641 - def __init__(self, filename: str, found_date: datetime, release_date: datetime | None, source: str) -> None: - self.filename = filename - self.found_date = found_date - self.release_date = release_date - self.source = source - - @classmethod - def from_dict(cls, dictionary: dict[str, str | None]) -> NewVersion: - return cls( - filename=dictionary["filename"], # ty: ignore[invalid-argument-type] - found_date=load_datetime(dictionary["found_date"]), # ty: ignore[invalid-argument-type] - release_date=load_datetime(dictionary["release_date"]), - source=dictionary["source"], # ty: ignore[invalid-argument-type] - ) - - def to_dict(self) -> dict[str, str | None]: - return { - "filename": self.filename, - "release_date": dump_datetime(self.release_date), - "found_date": dump_datetime(self.found_date), - "source": self.source, - } - - def use(self, now: datetime, ignore_grace_period_minor: bool = False, ignore_grace_period_ci: bool = False) -> bool: # noqa: FBT002 - if self.source == "manual": - return True - if self.source == "periodic" and (self.found_date < now - GRACE_PERIOD_CI or ignore_grace_period_ci): - if not ignore_grace_period_minor: - compare_from = self.release_date or self.found_date - return now - compare_from >= GRACE_PERIOD_MINOR - return True - return False - - def __repr__(self) -> str: - return ( - f"{self.__class__.__name__}(filename={self.filename}), found_date={self.found_date}, " - f"release_date={self.release_date}, source={self.source})" - ) - - def __eq__(self, other: object) -> bool: - return type(self) == type(other) and all( # noqa: E721 - getattr(self, k) == getattr(other, k) for k in ["filename", "release_date", "found_date", "source"] - ) - - def __ne__(self, other: object) -> bool: - return not (self == other) - - @property - def wheel(self) -> Wheel: - return Wheel(Path(self.filename)) - - -class UpdateLog: - def __init__( - self, started: datetime | None, completed: datetime | None, versions: list[NewVersion], periodic: bool | None - ) -> None: - self.started = started - self.completed = completed - self.versions = versions - self.periodic = periodic - - @classmethod - def from_dict(cls, dictionary: dict[str, object] | None) -> UpdateLog: - if dictionary is None: - dictionary = {} - return cls( - load_datetime(dictionary.get("started")), # ty: ignore[invalid-argument-type] - load_datetime(dictionary.get("completed")), # ty: ignore[invalid-argument-type] - [NewVersion.from_dict(v) for v in dictionary.get("versions", [])], # ty: ignore[not-iterable] - dictionary.get("periodic"), # ty: ignore[invalid-argument-type] - ) - - @classmethod - def from_app_data(cls, app_data: AppData, distribution: str, for_py_version: str) -> UpdateLog: - raw_json = app_data.embed_update_log(distribution, for_py_version).read() - return cls.from_dict(raw_json) - - def to_dict(self) -> dict[str, object]: - return { - "started": dump_datetime(self.started), - "completed": dump_datetime(self.completed), - "periodic": self.periodic, - "versions": [r.to_dict() for r in self.versions], - } - - @property - def needs_update(self) -> bool: - now = datetime.now(tz=timezone.utc) - if self.completed is None: # never completed - return self._check_start(now) - if now - self.completed <= UPDATE_PERIOD: - return False - return self._check_start(now) - - def _check_start(self, now: datetime) -> bool: - return self.started is None or now - self.started > UPDATE_ABORTED_DELAY - - -def trigger_update( # noqa: PLR0913 - distribution: str, - for_py_version: str, - wheel: Wheel | None, - search_dirs: list[Path], - app_data: AppData, - env: dict[str, str], - periodic: bool, -) -> None: - wheel_path = None if wheel is None else str(wheel.path) - cmd = [ - sys.executable, - "-c", - dedent( - """ - from virtualenv.report import setup_report, MAX_LEVEL - from virtualenv.seed.wheels.periodic_update import do_update - setup_report(MAX_LEVEL, show_pid=True) - do_update({!r}, {!r}, {!r}, {!r}, {!r}, {!r}) - """, - ) - .strip() - .format(distribution, for_py_version, wheel_path, str(app_data), [str(p) for p in search_dirs], periodic), - ] - debug = env.get("_VIRTUALENV_PERIODIC_UPDATE_INLINE") == "1" - pipe = None if debug else DEVNULL - kwargs = {"stdout": pipe, "stderr": pipe} - if not debug and sys.platform == "win32": - kwargs["creationflags"] = CREATE_NO_WINDOW - process = Popen(cmd, **kwargs) # ty: ignore[no-matching-overload] - LOGGER.info( - "triggered periodic upgrade of %s%s (for python %s) via background process having PID %d", - distribution, - "" if wheel is None else f"=={wheel.version}", - for_py_version, - process.pid, - ) - if debug: - process.communicate() # on purpose not called to make it a background process - else: - # set the returncode here -> no ResourceWarning on main process exit if the subprocess still runs - process.returncode = 0 - - -def do_update( # noqa: PLR0913 - distribution: str, - for_py_version: str, - embed_filename: str | None, - app_data: str | AppData, - search_dirs: list[str] | list[Path], - periodic: bool, -) -> list[NewVersion] | None: - versions = None - try: - versions = _run_do_update(app_data, distribution, embed_filename, for_py_version, periodic, search_dirs) - finally: - LOGGER.debug("done %s %s with %s", distribution, for_py_version, versions) - return versions - - -def _run_do_update( # noqa: C901, PLR0913 - app_data: str | AppData, - distribution: str, - embed_filename: str | None, - for_py_version: str, - periodic: bool, - search_dirs: list[str] | list[Path], -) -> list[NewVersion]: - from virtualenv.seed.wheels import acquire # noqa: PLC0415 - - wheel_filename = None if embed_filename is None else Path(embed_filename) - embed_version = None if wheel_filename is None else Wheel(wheel_filename).version_tuple - app_data = AppDataDiskFolder(app_data) if isinstance(app_data, str) else app_data - search_dirs = [Path(p) if isinstance(p, str) else p for p in search_dirs] - wheelhouse = app_data.house - embed_update_log = app_data.embed_update_log(distribution, for_py_version) - u_log = UpdateLog.from_dict(embed_update_log.read()) - now = datetime.now(tz=timezone.utc) - - update_versions, other_versions = [], [] - for version in u_log.versions: - if version.source in {"periodic", "manual"}: - update_versions.append(version) - else: - other_versions.append(version) - - if periodic: - source = "periodic" - else: - source = "manual" - # mark the most recent one as source "manual" - if update_versions: - update_versions[0].source = source - - if wheel_filename is not None: - dest = wheelhouse / wheel_filename.name - if not dest.exists(): - copy2(str(wheel_filename), str(wheelhouse)) - last, last_version, versions, filenames = None, None, [], set() - while last is None or not last.use(now, ignore_grace_period_ci=True): - download_time = datetime.now(tz=timezone.utc) - dest = acquire.download_wheel( - distribution=distribution, - version_spec=None if last_version is None else f"<{last_version}", - for_py_version=for_py_version, - search_dirs=search_dirs, - app_data=app_data, - to_folder=wheelhouse, - env=os.environ, # ty: ignore[invalid-argument-type] - ) - if dest is None or (update_versions and update_versions[0].filename == dest.name): - break - release_date = release_date_for_wheel_path(dest.path) - last = NewVersion(filename=dest.path.name, release_date=release_date, found_date=download_time, source=source) - LOGGER.info("detected %s in %s", last, datetime.now(tz=timezone.utc) - download_time) - versions.append(last) - filenames.add(last.filename) - last_wheel = last.wheel - last_version = last_wheel.version - if embed_version is not None and embed_version >= last_wheel.version_tuple: - break # stop download if we reach the embed version - u_log.periodic = periodic - if not u_log.periodic: - u_log.started = now - # update other_versions by removing version we just found - other_versions = [version for version in other_versions if version.filename not in filenames] - u_log.versions = versions + update_versions + other_versions - u_log.completed = datetime.now(tz=timezone.utc) - embed_update_log.write(u_log.to_dict()) - return versions - - -def release_date_for_wheel_path(dest: Path) -> datetime | None: - wheel = Wheel(dest) - # the most accurate is to ask PyPi - e.g. https://pypi.org/pypi/pip/json, - # see https://warehouse.pypa.io/api-reference/json/ for more details - content = _pypi_get_distribution_info_cached(wheel.distribution) - if content is not None: - try: - upload_time = content["releases"][wheel.version][0]["upload_time"] # ty: ignore[not-subscriptable] - return datetime.strptime(upload_time, "%Y-%m-%dT%H:%M:%S").replace(tzinfo=timezone.utc) - except Exception as exception: # noqa: BLE001 - LOGGER.error("could not load release date %s because %r", content, exception) # noqa: TRY400 - return None - - -def _request_context() -> Generator[ssl.SSLContext | None, None, None]: - yield None - # fallback to non verified HTTPS (the information we request is not sensitive, so fallback) - yield ssl._create_unverified_context() # noqa: S323, SLF001 - - -_PYPI_CACHE = {} - - -def _pypi_get_distribution_info_cached(distribution: str) -> dict[str, object] | None: - if distribution not in _PYPI_CACHE: - _PYPI_CACHE[distribution] = _pypi_get_distribution_info(distribution) - return _PYPI_CACHE[distribution] - - -def _pypi_get_distribution_info(distribution: str) -> dict[str, object] | None: - content, url = None, f"https://pypi.org/pypi/{distribution}/json" - try: - for context in _request_context(): - try: - with urlopen(url, context=context) as file_handler: - content = json.load(file_handler) - break - except URLError as exception: - LOGGER.error("failed to access %s because %r", url, exception) # noqa: TRY400 - except Exception as exception: # noqa: BLE001 - LOGGER.error("failed to access %s because %r", url, exception) # noqa: TRY400 - return content - - -def manual_upgrade(app_data: AppData, env: dict[str, str]) -> None: - threads = [] - - for for_py_version, distribution_to_package in BUNDLE_SUPPORT.items(): - # load extra search dir for the given for_py - for distribution in distribution_to_package: - thread = Thread(target=_run_manual_upgrade, args=(app_data, distribution, for_py_version, env)) - thread.start() - threads.append(thread) - - for thread in threads: - thread.join() - - -def _run_manual_upgrade(app_data: AppData, distribution: str, for_py_version: str, env: dict[str, str]) -> None: - start = datetime.now(tz=timezone.utc) - from .bundle import from_bundle # noqa: PLC0415 - - current = from_bundle( - distribution=distribution, - version=None, - for_py_version=for_py_version, - search_dirs=[], - app_data=app_data, - do_periodic_update=False, - env=env, - ) - LOGGER.warning( - "upgrade %s for python %s with current %s", - distribution, - for_py_version, - "" if current is None else current.name, - ) - versions = do_update( - distribution=distribution, - for_py_version=for_py_version, - embed_filename=current.path, # ty: ignore[invalid-argument-type, unresolved-attribute] - app_data=app_data, - search_dirs=[], - periodic=False, - ) - - args = [ - distribution, - for_py_version, - datetime.now(tz=timezone.utc) - start, - ] - if versions: - args.append("\n".join(f"\t{v}" for v in versions)) - ver_update = "new entries found:\n%s" if versions else "no new versions found" - msg = f"upgraded %s for python %s in %s {ver_update}" - LOGGER.warning(msg, *args) - - -__all__ = [ - "NewVersion", - "UpdateLog", - "add_wheel_to_update_log", - "do_update", - "dump_datetime", - "load_datetime", - "manual_upgrade", - "periodic_update", - "release_date_for_wheel_path", - "trigger_update", -] diff --git a/src/virtualenv/seed/wheels/util.py b/src/virtualenv/seed/wheels/util.py deleted file mode 100644 index ff7721576..000000000 --- a/src/virtualenv/seed/wheels/util.py +++ /dev/null @@ -1,125 +0,0 @@ -from __future__ import annotations - -from operator import attrgetter -from typing import TYPE_CHECKING -from zipfile import ZipFile - -if TYPE_CHECKING: - from pathlib import Path - - -class Wheel: - def __init__(self, path: Path) -> None: - # https://www.python.org/dev/peps/pep-0427/#file-name-convention - # The wheel filename is {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl - self.path = path - self._parts = path.stem.split("-") - - @classmethod - def from_path(cls, path: Path) -> Wheel | None: - if path is not None and path.suffix == ".whl" and len(path.stem.split("-")) >= 5: # noqa: PLR2004 - return cls(path) - return None - - @property - def distribution(self) -> str: - return self._parts[0] - - @property - def version(self) -> str: - return self._parts[1] - - @property - def version_tuple(self) -> tuple[int, ...]: - return self.as_version_tuple(self.version) - - @staticmethod - def as_version_tuple(version: str) -> tuple[int, ...]: - result = [] - for part in version.split(".")[0:3]: - try: - result.append(int(part)) - except ValueError: # noqa: PERF203 - break - if not result: - raise ValueError(version) - return tuple(result) - - @property - def name(self) -> str: - return self.path.name - - def support_py(self, py_version: str) -> bool: - name = f"{'-'.join(self.path.stem.split('-')[0:2])}.dist-info/METADATA" - with ZipFile(str(self.path), "r") as zip_file: - metadata = zip_file.read(name).decode("utf-8") - marker = "Requires-Python:" - requires = next((i[len(marker) :] for i in metadata.splitlines() if i.startswith(marker)), None) - if requires is None: # if it does not specify a python requires the assumption is compatible - return True - py_version_int = tuple(int(i) for i in py_version.split(".")) - for require in (i.strip() for i in requires.split(",")): - # https://www.python.org/dev/peps/pep-0345/#version-specifiers - for operator, check in [ - ("!=", lambda v: py_version_int != v), - ("==", lambda v: py_version_int == v), - ("<=", lambda v: py_version_int <= v), - (">=", lambda v: py_version_int >= v), - ("<", lambda v: py_version_int < v), - (">", lambda v: py_version_int > v), - ]: - if require.startswith(operator): - ver_str = require[len(operator) :].strip() - version = tuple((int(i) if i != "*" else None) for i in ver_str.split("."))[0:2] - if not check(version): - return False - break - return True - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.path})" - - def __str__(self) -> str: - return str(self.path) - - -def discover_wheels(from_folder: Path, distribution: str, version: str | None, for_py_version: str) -> list[Wheel]: - wheels = [] - for filename in from_folder.iterdir(): - wheel = Wheel.from_path(filename) - if ( - wheel - and wheel.distribution == distribution - and (version is None or wheel.version == version) - and wheel.support_py(for_py_version) - ): - wheels.append(wheel) - return sorted(wheels, key=attrgetter("version_tuple", "distribution"), reverse=True) - - -class Version: - #: the version bundled with virtualenv - bundle = "bundle" - embed = "embed" - #: custom version handlers - non_version = (bundle, embed) - - @staticmethod - def of_version(value: str | None) -> str | None: - return None if value in Version.non_version else value - - @staticmethod - def as_pip_req(distribution: str, version: str | None) -> str: - return f"{distribution}{Version.as_version_spec(version)}" - - @staticmethod - def as_version_spec(version: str | None) -> str: - of_version = Version.of_version(version) - return "" if of_version is None else f"=={of_version}" - - -__all__ = [ - "Version", - "Wheel", - "discover_wheels", -] diff --git a/src/virtualenv/util/__init__.py b/src/virtualenv/util/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/virtualenv/util/error.py b/src/virtualenv/util/error.py deleted file mode 100644 index d58a989ac..000000000 --- a/src/virtualenv/util/error.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Errors.""" - -from __future__ import annotations - - -class ProcessCallFailedError(RuntimeError): - """Failed a process call.""" - - def __init__(self, code: int, out: str, err: str, cmd: list[str]) -> None: - super().__init__(code, out, err, cmd) - self.code = code - self.out = out - self.err = err - self.cmd = cmd diff --git a/src/virtualenv/util/lock.py b/src/virtualenv/util/lock.py deleted file mode 100644 index 51f6cc97d..000000000 --- a/src/virtualenv/util/lock.py +++ /dev/null @@ -1,188 +0,0 @@ -"""holds locking functionality that works across processes.""" - -from __future__ import annotations - -import logging -import os -from abc import ABC, abstractmethod -from contextlib import contextmanager, suppress -from pathlib import Path -from threading import Lock, RLock -from typing import TYPE_CHECKING - -from filelock import FileLock, Timeout - -if TYPE_CHECKING: - from collections.abc import Iterator - from types import TracebackType - -LOGGER = logging.getLogger(__name__) - - -class _CountedFileLock(FileLock): - def __init__(self, lock_file: str) -> None: - parent = os.path.dirname(lock_file) - with suppress(OSError): - os.makedirs(parent, exist_ok=True) - - super().__init__(lock_file) - self.count = 0 - self.thread_safe = RLock() - - def acquire( # ty: ignore[invalid-method-override] - self, - timeout: float | None = None, - poll_interval: float = 0.05, - ) -> None: - if not self.thread_safe.acquire(timeout=-1 if timeout is None else timeout): - raise Timeout(self.lock_file) - if self.count == 0: - try: - super().acquire(timeout, poll_interval) - except BaseException: - self.thread_safe.release() - raise - self.count += 1 - - def release(self, force: bool = False) -> None: # noqa: FBT002 - with self.thread_safe: - if self.count > 0: - if self.count == 1: - super().release(force=force) - self.count -= 1 - if self.count == 0: - # if we have no more users of this lock, release the thread lock - self.thread_safe.release() - - -_lock_store = {} -_store_lock = Lock() - - -class PathLockBase(ABC): - def __init__(self, folder: str | Path) -> None: - path = Path(folder) - self.path = path.resolve() if path.exists() else path - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.path})" - - def __truediv__(self, other: str) -> PathLockBase: - return type(self)(self.path / other) - - @abstractmethod - def __enter__(self) -> None: - raise NotImplementedError - - @abstractmethod - def __exit__( - self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None - ) -> None: - raise NotImplementedError - - @abstractmethod - @contextmanager - def lock_for_key(self, name: str, no_block: bool = False) -> Iterator[None]: # noqa: FBT002 - raise NotImplementedError - - @abstractmethod - @contextmanager - def non_reentrant_lock_for_key(self, name: str) -> Iterator[None]: - raise NotImplementedError - - -class ReentrantFileLock(PathLockBase): - def __init__(self, folder: str | Path) -> None: - super().__init__(folder) - self._lock = None - - def _create_lock(self, name: str = "") -> _CountedFileLock: - lock_file = str(self.path / f"{name}.lock") - with _store_lock: - if lock_file not in _lock_store: - _lock_store[lock_file] = _CountedFileLock(lock_file) - return _lock_store[lock_file] - - @staticmethod - def _del_lock(lock: _CountedFileLock | None) -> None: - if lock is not None: - with _store_lock, lock.thread_safe: - if lock.count == 0: - _lock_store.pop(lock.lock_file, None) - - def __del__(self) -> None: - self._del_lock(self._lock) - - def __enter__(self) -> None: - self._lock = self._create_lock() - self._lock_file(self._lock) - - def __exit__( - self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None - ) -> None: - self._release(self._lock) # ty: ignore[invalid-argument-type] - self._del_lock(self._lock) - self._lock = None - - def _lock_file(self, lock: _CountedFileLock, no_block: bool = False) -> None: # noqa: FBT002 - # multiple processes might be trying to get a first lock... so we cannot check if this directory exist without - # a lock, but that lock might then become expensive, and it's not clear where that lock should live. - # Instead here we just ignore if we fail to create the directory. - with suppress(OSError): - os.makedirs(str(self.path), exist_ok=True) - - try: - lock.acquire(0.0001) - except Timeout: - if no_block: - raise - LOGGER.debug("lock file %s present, will block until released", lock.lock_file) - lock.release() # release the acquire try from above - lock.acquire() - - @staticmethod - def _release(lock: _CountedFileLock) -> None: - lock.release() - - @contextmanager - def lock_for_key(self, name: str, no_block: bool = False) -> Iterator[None]: # noqa: FBT002 - lock = self._create_lock(name) - try: - try: - self._lock_file(lock, no_block) - yield - finally: - self._release(lock) - finally: - self._del_lock(lock) - lock = None - - @contextmanager - def non_reentrant_lock_for_key(self, name: str) -> Iterator[None]: - with _CountedFileLock(str(self.path / f"{name}.lock")): - yield - - -class NoOpFileLock(PathLockBase): - def __enter__(self) -> None: - raise NotImplementedError - - def __exit__( - self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None - ) -> None: - raise NotImplementedError - - @contextmanager - def lock_for_key(self, name: str, no_block: bool = False) -> Iterator[None]: # noqa: ARG002, FBT002 - yield - - @contextmanager - def non_reentrant_lock_for_key(self, name: str) -> Iterator[None]: # noqa: ARG002 - yield - - -__all__ = [ - "NoOpFileLock", - "ReentrantFileLock", - "Timeout", -] diff --git a/src/virtualenv/util/path/__init__.py b/src/virtualenv/util/path/__init__.py deleted file mode 100644 index c144d8dfd..000000000 --- a/src/virtualenv/util/path/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from __future__ import annotations - -from ._permission import make_exe, set_tree -from ._sync import copy, copytree, ensure_dir, safe_delete, symlink -from ._win import get_short_path_name - -__all__ = [ - "copy", - "copytree", - "ensure_dir", - "get_short_path_name", - "make_exe", - "safe_delete", - "set_tree", - "symlink", -] diff --git a/src/virtualenv/util/path/_permission.py b/src/virtualenv/util/path/_permission.py deleted file mode 100644 index 70bb24b6e..000000000 --- a/src/virtualenv/util/path/_permission.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import annotations - -import os -from stat import S_IXGRP, S_IXOTH, S_IXUSR -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from pathlib import Path - - -def make_exe(filename: Path) -> None: - original_mode = filename.stat().st_mode - levels = [S_IXUSR, S_IXGRP, S_IXOTH] - for at in range(len(levels), 0, -1): - try: - mode = original_mode - for level in levels[:at]: - mode |= level - filename.chmod(mode) - break - except OSError: - continue - - -def set_tree(folder: Path, stat: int) -> None: - for root, _, files in os.walk(str(folder)): - for filename in files: - os.chmod(os.path.join(root, filename), stat) - - -__all__ = ( - "make_exe", - "set_tree", -) diff --git a/src/virtualenv/util/path/_sync.py b/src/virtualenv/util/path/_sync.py deleted file mode 100644 index 6d701b906..000000000 --- a/src/virtualenv/util/path/_sync.py +++ /dev/null @@ -1,90 +0,0 @@ -from __future__ import annotations - -import logging -import os -import shutil -import sys -from stat import S_IWUSR -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from pathlib import Path - -LOGGER = logging.getLogger(__name__) - - -def ensure_dir(path: Path) -> None: - if not path.exists(): - LOGGER.debug("create folder %s", path) - os.makedirs(str(path)) - - -def ensure_safe_to_do(src: Path, dest: Path) -> None: - if src == dest: - msg = f"source and destination is the same {src}" - raise ValueError(msg) - if not dest.exists(): - return - if dest.is_dir() and not dest.is_symlink(): - LOGGER.debug("remove directory %s", dest) - safe_delete(dest) - else: - LOGGER.debug("remove file %s", dest) - dest.unlink() - - -def symlink(src: Path, dest: Path) -> None: - ensure_safe_to_do(src, dest) - LOGGER.debug("symlink %s", _Debug(src, dest)) - dest.symlink_to(src, target_is_directory=src.is_dir()) - - -def copy(src: Path, dest: Path) -> None: - ensure_safe_to_do(src, dest) - is_dir = src.is_dir() - method = copytree if is_dir else shutil.copy - LOGGER.debug("copy %s", _Debug(src, dest)) - method(str(src), str(dest)) - - -def copytree(src: str, dest: str) -> None: - for root, _, files in os.walk(src): - dest_dir = os.path.join(dest, os.path.relpath(root, src)) - if not os.path.isdir(dest_dir): - os.makedirs(dest_dir) - for name in files: - src_f = os.path.join(root, name) - dest_f = os.path.join(dest_dir, name) - shutil.copy(src_f, dest_f) - - -def safe_delete(dest: Path) -> None: - def onerror(func: object, path: str, exc_info: object) -> None: # noqa: ARG001 - if not os.access(path, os.W_OK): - os.chmod(path, S_IWUSR) - func(path) # ty: ignore[call-non-callable] - else: - raise # noqa: PLE0704 - - if sys.version_info >= (3, 12): - shutil.rmtree(str(dest), ignore_errors=True, onexc=onerror) - else: - shutil.rmtree(str(dest), ignore_errors=True, onerror=onerror) - - -class _Debug: - def __init__(self, src: Path, dest: Path) -> None: - self.src = src - self.dest = dest - - def __str__(self) -> str: - return f"{'directory ' if self.src.is_dir() else ''}{self.src!s} to {self.dest!s}" - - -__all__ = [ - "copy", - "copytree", - "ensure_dir", - "safe_delete", - "symlink", -] diff --git a/src/virtualenv/util/path/_win.py b/src/virtualenv/util/path/_win.py deleted file mode 100644 index 5498c0130..000000000 --- a/src/virtualenv/util/path/_win.py +++ /dev/null @@ -1,23 +0,0 @@ -from __future__ import annotations - - -def get_short_path_name(long_name: str) -> str: - """Gets the short path name of a given long path - http://stackoverflow.com/a/23598461/200291.""" - import ctypes # noqa: PLC0415 - from ctypes import wintypes # noqa: PLC0415 - - GetShortPathNameW = ctypes.windll.kernel32.GetShortPathNameW # noqa: N806 # ty: ignore[unresolved-attribute] - GetShortPathNameW.argtypes = [wintypes.LPCWSTR, wintypes.LPWSTR, wintypes.DWORD] - GetShortPathNameW.restype = wintypes.DWORD - output_buf_size = 0 - while True: - output_buf = ctypes.create_unicode_buffer(output_buf_size) - needed = GetShortPathNameW(long_name, output_buf, output_buf_size) - if output_buf_size >= needed: - return output_buf.value - output_buf_size = needed - - -__all__ = [ - "get_short_path_name", -] diff --git a/src/virtualenv/util/subprocess/__init__.py b/src/virtualenv/util/subprocess/__init__.py deleted file mode 100644 index 3398ac2c5..000000000 --- a/src/virtualenv/util/subprocess/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ -from __future__ import annotations - -import subprocess -from shlex import quote -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Mapping - -CREATE_NO_WINDOW = 0x80000000 - - -class LogCmd: - def __init__(self, cmd: list[str], env: Mapping[str, str] | None = None) -> None: - self.cmd = cmd - self.env = env - - def __repr__(self) -> str: - cmd_repr = " ".join(quote(str(c)) for c in self.cmd) - if self.env is not None: - cmd_repr = f"{cmd_repr} env of {self.env!r}" - return cmd_repr - - -def run_cmd(cmd: list[str]) -> tuple[int, str, str]: - try: - process = subprocess.Popen( - cmd, - universal_newlines=True, - stdin=subprocess.PIPE, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - encoding="utf-8", - ) - out, err = process.communicate() # input disabled - code = process.returncode - except OSError as error: - code, out, err = error.errno, "", error.strerror - if code == 2 and err is not None and "file" in err: # noqa: PLR2004 - err = str(error) # FileNotFoundError in Python >= 3.3 - return code, out, err # ty: ignore[invalid-return-type] - - -__all__ = ( - "CREATE_NO_WINDOW", - "LogCmd", - "run_cmd", -) diff --git a/src/virtualenv/util/zipapp.py b/src/virtualenv/util/zipapp.py deleted file mode 100644 index 3e591344e..000000000 --- a/src/virtualenv/util/zipapp.py +++ /dev/null @@ -1,47 +0,0 @@ -from __future__ import annotations - -import logging -import os -import zipfile -from typing import TYPE_CHECKING - -from virtualenv.info import IS_WIN, ROOT - -if TYPE_CHECKING: - from pathlib import Path - -LOGGER = logging.getLogger(__name__) - - -def read(full_path: str | Path) -> str: - sub_file = _get_path_within_zip(full_path) - with zipfile.ZipFile(ROOT, "r") as zip_file, zip_file.open(sub_file) as file_handler: - return file_handler.read().decode("utf-8") - - -def extract(full_path: str | Path, dest: Path) -> None: - LOGGER.debug("extract %s to %s", full_path, dest) - sub_file = _get_path_within_zip(full_path) - with zipfile.ZipFile(ROOT, "r") as zip_file: - info = zip_file.getinfo(sub_file) - info.filename = dest.name - zip_file.extract(info, str(dest.parent)) - - -def _get_path_within_zip(full_path: str | Path) -> str: - full_path = os.path.realpath(os.path.abspath(str(full_path))) - prefix = f"{ROOT}{os.sep}" - if not full_path.startswith(prefix): - msg = f"full_path={full_path} should start with prefix={prefix}." - raise RuntimeError(msg) - sub_file = full_path[len(prefix) :] - if IS_WIN: - # paths are always UNIX separators, even on Windows, though __file__ still follows platform default - sub_file = sub_file.replace(os.sep, "/") - return sub_file - - -__all__ = [ - "extract", - "read", -] diff --git a/tasks/__main__zipapp.py b/tasks/__main__zipapp.py deleted file mode 100644 index 1612a8b62..000000000 --- a/tasks/__main__zipapp.py +++ /dev/null @@ -1,188 +0,0 @@ -from __future__ import annotations - -import json -import os -import sys -import zipfile -from functools import cached_property -from importlib.abc import SourceLoader -from importlib.util import spec_from_file_location -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from collections.abc import Iterator - from importlib.machinery import ModuleSpec - from types import ModuleType, TracebackType - from typing import Self - -ABS_HERE = os.path.abspath(os.path.dirname(__file__)) - - -class VersionPlatformSelect: - def __init__(self) -> None: - zipapp = ABS_HERE - self.archive = zipapp - self._zip_file = zipfile.ZipFile(zipapp) - self.modules = self._load("modules.json") - self.distributions = self._load("distributions.json") - self.__cache = {} - - def _load(self, of_file: str) -> dict[str, str]: - version = ".".join(str(i) for i in sys.version_info[0:2]) - per_version = json.loads(self.get_data(of_file).decode()) - all_platforms = per_version[version] if version in per_version else per_version["3.9"] - content = all_platforms.get("==any", {}) # start will all platforms - not_us = f"!={sys.platform}" - for key, value in all_platforms.items(): # now override that with not platform - if key.startswith("!=") and key != not_us: - content.update(value) - content.update(all_platforms.get(f"=={sys.platform}", {})) # and finish it off with our platform - return content - - def __enter__(self) -> Self: - return self - - def __exit__( - self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None - ) -> None: - self._zip_file.close() - - def find_mod(self, fullname: str) -> str | None: - if fullname in self.modules: - return self.modules[fullname] - return None - - def get_filename(self, fullname: str) -> str | None: - zip_path = self.find_mod(fullname) - return None if zip_path is None else os.path.join(ABS_HERE, zip_path) - - def get_data(self, filename: str) -> bytes: - if filename.startswith(ABS_HERE): - # keep paths relative from the zipfile - filename = filename[len(ABS_HERE) + 1 :] - filename = filename.lstrip(os.sep) - if sys.platform == "win32": - # paths within the zipfile is always /, fixup on Windows to transform \ to / - filename = "/".join(filename.split(os.sep)) - with self._zip_file.open(filename) as file_handler: - return file_handler.read() - - def find_distributions(self, context: Any) -> Iterator[Any]: # noqa: ANN401 - dist_class = versioned_distribution_class() - if context.name is None: - return - name = context.name.replace("_", "-") - if name in self.distributions: - yield dist_class(file_loader=self.get_data, dist_path=self.distributions[name]) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(path={ABS_HERE})" - - def _register_distutils_finder(self) -> None: # noqa: C901 - if "distlib" not in self.modules: - return - - class Resource: - def __init__(self, path: str, name: str, loader: SourceLoader) -> None: - self.path = os.path.join(path, name) - self._name = name - self.loader = loader - - @cached_property - def name(self) -> str: - return os.path.basename(self._name) - - @property - def bytes(self) -> bytes: - return self.loader.get_data(self._name) - - @property - def is_container(self) -> bool: - return len(self.resources) > 1 - - @cached_property - def resources(self) -> list[str]: - return [ - i.filename - for i in ( - (j for j in zip_file.filelist if j.filename.startswith(f"{self._name}/")) - if self._name - else zip_file.filelist - ) - ] - - class DistlibFinder: - def __init__(self, path: str, loader: Any) -> None: # noqa: ANN401 - self.path = path - self.loader = loader - - def find(self, name: str) -> Any: # noqa: ANN401 - return Resource(self.path, name, self.loader) - - def iterator(self, resource_name: str) -> Iterator[Any]: - resource = self.find(resource_name) - if resource is not None: - todo = [resource] - while todo: - resource = todo.pop(0) - yield resource - if resource.is_container: - resource_name = resource.name - for name in resource.resources: - child = self.find(f"{resource_name}/{name}" if resource_name else name) - if child.is_container: - todo.append(child) - else: - yield child - - from distlib.resources import register_finder # noqa: PLC0415 - - zip_file = self._zip_file - register_finder(self, lambda module: DistlibFinder(os.path.dirname(module.__file__), self)) - - -_VER_DISTRIBUTION_CLASS = None - - -def versioned_distribution_class() -> type: - global _VER_DISTRIBUTION_CLASS # noqa: PLW0603 - if _VER_DISTRIBUTION_CLASS is None: - from importlib.metadata import Distribution # noqa: PLC0415 - - class VersionedDistribution(Distribution): - def __init__(self, file_loader: Any, dist_path: str) -> None: # noqa: ANN401 - self.file_loader = file_loader - self.dist_path = dist_path - - def read_text(self, filename: str) -> str: - return self.file_loader(self.locate_file(filename)).decode("utf-8") - - def locate_file(self, path: str) -> str: - return os.path.join(self.dist_path, path) - - _VER_DISTRIBUTION_CLASS = VersionedDistribution - return _VER_DISTRIBUTION_CLASS - - -class VersionedFindLoad(VersionPlatformSelect, SourceLoader): - def find_spec(self, fullname: str, path: Any, target: ModuleType | None = None) -> ModuleSpec | None: # noqa: ARG002, ANN401 - zip_path = self.find_mod(fullname) - if zip_path is not None: - return spec_from_file_location(name=fullname, loader=self) - return None - - def module_repr(self, module: ModuleType) -> str: - raise NotImplementedError - - -def run() -> None: - with VersionedFindLoad() as finder: - sys.meta_path.insert(0, finder) - finder._register_distutils_finder() # noqa: SLF001 - from virtualenv.__main__ import run as run_virtualenv # noqa: PLC0415 - - run_virtualenv() - - -if __name__ == "__main__": - run() diff --git a/tasks/make_zipapp.py b/tasks/make_zipapp.py deleted file mode 100644 index 62e583555..000000000 --- a/tasks/make_zipapp.py +++ /dev/null @@ -1,300 +0,0 @@ -"""https://docs.python.org/3/library/zipapp.html.""" - -from __future__ import annotations - -import argparse -import io -import json -import os -import shutil -import subprocess -import sys -import zipapp -import zipfile -from collections import defaultdict, deque -from email import message_from_string -from pathlib import Path, PurePosixPath -from shlex import quote -from stat import S_IWUSR -from tempfile import TemporaryDirectory -from typing import TYPE_CHECKING, Any - -from packaging.markers import Marker -from packaging.requirements import Requirement - -if TYPE_CHECKING: - from collections.abc import Iterator - -HERE = Path(__file__).parent.absolute() - -VERSIONS = [f"3.{i}" for i in range(14, 7, -1)] - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--dest", default="virtualenv.pyz") - args = parser.parse_args() - with TemporaryDirectory() as folder: - packages = get_wheels_for_support_versions(Path(folder)) - create_zipapp(os.path.abspath(args.dest), packages) - - -def create_zipapp(dest: str, packages: dict[str, Any]) -> None: - bio = io.BytesIO() - base = PurePosixPath("__virtualenv__") - modules = defaultdict(lambda: defaultdict(dict)) - dist = defaultdict(lambda: defaultdict(dict)) - with zipfile.ZipFile(bio, "w") as zip_app: - write_packages_to_zipapp(base, dist, modules, packages, zip_app) - modules_json = json.dumps(modules, indent=2) - zip_app.writestr("modules.json", modules_json) - distributions_json = json.dumps(dist, indent=2) - zip_app.writestr("distributions.json", distributions_json) - zip_app.writestr("__main__.py", (HERE / "__main__zipapp.py").read_bytes()) - bio.seek(0) - zipapp.create_archive(bio, dest) - print(f"zipapp created at {dest} with size {os.path.getsize(dest) / 1024 / 1024:.2f}MB") # noqa: T201 - - -def write_packages_to_zipapp( # noqa: C901, PLR0912 - base: PurePosixPath, - dist: dict[str, Any], - modules: dict[str, Any], - packages: dict[str, Any], - zip_app: zipfile.ZipFile, -) -> None: - has = set() - for name, p_w_v in packages.items(): # noqa: PLR1702 - for platform, w_v in p_w_v.items(): - for wheel_data in w_v.values(): - wheel = wheel_data.wheel - with zipfile.ZipFile(str(wheel)) as wheel_zip: - for filename in wheel_zip.namelist(): - if name == "virtualenv": - dest = PurePosixPath(filename) - else: - dest = base / wheel.stem / filename - if dest.suffix in {".so", ".pyi"}: - continue - if dest.suffix == ".py": - key = filename[:-3].replace("/", ".").replace("__init__", "").rstrip(".") - for version in wheel_data.versions: - modules[version][platform][key] = str(dest) - if dest.parent.suffix == ".dist-info": - dist_name = dest.parent.stem.split("-")[0].replace("_", "-") - for version in wheel_data.versions: - dist[version][platform][dist_name] = str(dest.parent) - dest_str = str(dest) - if dest_str in has: - continue - has.add(dest_str) - if "/tests/" in dest_str or "/docs/" in dest_str: - continue - print(dest_str) # noqa: T201 - content = wheel_zip.read(filename) - zip_app.writestr(dest_str, content) - del content - - -class WheelDownloader: - def __init__(self, into: Path) -> None: - if into.exists(): - shutil.rmtree(into) - into.mkdir(parents=True) - self.into = into - self.collected = defaultdict(lambda: defaultdict(dict)) - self.pip_cmd = [str(Path(sys.executable).parent / "pip")] - self._cmd = [*self.pip_cmd, "download", "-q", "--no-deps", "--no-cache-dir", "--dest", str(self.into)] - - def run(self, target: Path, versions: list[str]) -> None: - whl = self.build_sdist(target) - todo = deque((version, None, whl) for version in versions) - wheel_store = {} - while todo: - version, platform, dep = todo.popleft() - dep_str = dep.name.split("-")[0] if isinstance(dep, Path) else dep.name - if dep_str in self.collected[version] and platform in self.collected[version][dep_str]: - continue - whl = self._get_wheel(dep, platform[2:] if platform and platform.startswith("==") else None, version) - if whl is None: - if dep_str not in wheel_store: - msg = f"failed to get {dep_str}, have {wheel_store}" - raise RuntimeError(msg) - whl = wheel_store[dep_str] - else: - wheel_store[dep_str] = whl - self.collected[version][dep_str][platform] = whl - todo.extend(self.get_dependencies(whl, version)) - - def _get_wheel(self, dep: Requirement | Path, platform: str | None, version: str) -> Path | None: - if isinstance(dep, Requirement): - before = set(self.into.iterdir()) - if self._download( - platform, - False, # noqa: FBT003 - "--python-version", - version, - "--only-binary", - ":all:", - str(dep), - ): - self._download(platform, True, "--python-version", version, str(dep)) # noqa: FBT003 - after = set(self.into.iterdir()) - new_files = after - before - assert len(new_files) <= 1 # noqa: S101 - if not len(new_files): - return None - new_file = next(iter(new_files)) - if new_file.suffix == ".whl": - return new_file - dep = new_file - new_file = self.build_sdist(dep) - assert new_file.suffix == ".whl" # noqa: S101 - return new_file - - def _download(self, platform: str | None, stop_print_on_fail: bool, *args: str) -> int: - exe_cmd = self._cmd + list(args) - if platform is not None: - exe_cmd.extend(["--platform", platform]) - return run_suppress_output(exe_cmd, stop_print_on_fail=stop_print_on_fail) - - @staticmethod - def get_dependencies(whl: Path, version: str) -> Iterator[tuple[str, str | None, Requirement]]: - with zipfile.ZipFile(str(whl), "r") as zip_file: - name = "/".join([f"{'-'.join(whl.name.split('-')[0:2])}.dist-info", "METADATA"]) - with zip_file.open(name) as file_handler: - metadata = message_from_string(file_handler.read().decode("utf-8")) - deps = metadata.get_all("Requires-Dist") - if deps is None: - return - for dep in deps: - req = Requirement(dep) - markers = getattr(req.marker, "_markers", ()) or () - if any( - m - for m in markers - if isinstance(m, tuple) and len(m) == 3 and m[0].value == "extra" # noqa: PLR2004 - ): - continue - py_versions = WheelDownloader._marker_at(markers, "python_version") - if py_versions: - marker = Marker('python_version < "1"') - marker._markers = [ # noqa: SLF001 - markers[ver] for ver in sorted(i for i in set(py_versions) | {i - 1 for i in py_versions} if i >= 0) - ] - matches_python = marker.evaluate({"python_version": version}) - if not matches_python: - continue - deleted = 0 - for ver in py_versions: - deleted += WheelDownloader._del_marker_at(markers, ver - deleted) - platforms = [] - platform_positions = WheelDownloader._marker_at(markers, "sys_platform") - deleted = 0 - for pos in platform_positions: # can only be or meaningfully - platform = f"{markers[pos][1].value}{markers[pos][2].value}" - deleted += WheelDownloader._del_marker_at(markers, pos - deleted) - platforms.append(platform) - if not platforms: - platforms.append(None) - for platform in platforms: - yield version, platform, req - - @staticmethod - def _marker_at(markers: list[Any], key: str) -> list[int]: - return [ - i - for i, m in enumerate(markers) - if isinstance(m, tuple) and len(m) == 3 and m[0].value == key # noqa: PLR2004 - ] - - @staticmethod - def _del_marker_at(markers: list[Any], at: int) -> int: - del markers[at] - deleted = 1 - op = max(at - 1, 0) - if markers and isinstance(markers[op], str): - del markers[op] - deleted += 1 - return deleted - - def build_sdist(self, target: Path) -> Path: - if target.is_dir(): - # pip 20.1 no longer guarantees this to be parallel safe, need to copy/lock - with TemporaryDirectory() as temp_folder: - folder = Path(temp_folder) / target.name - shutil.copytree( - str(target), - str(folder), - ignore=shutil.ignore_patterns(".tox", ".tox4", "venv", "__pycache__", "*.pyz"), - ) - try: - return self._build_sdist(self.into, folder) - finally: - # permission error on Windows <3.7 https://bugs.python.org/issue26660 - def onerror(func: Any, path: str, exc_info: Any) -> None: # noqa: ARG001, ANN401 - os.chmod(path, S_IWUSR) - func(path) - - shutil.rmtree(str(folder), onerror=onerror) - - else: - return self._build_sdist(target.parent / target.stem, target) - - def _build_sdist(self, folder: Path, target: Path) -> Path: - if not folder.exists() or not list(folder.iterdir()): - cmd = [*self.pip_cmd, "wheel", "-w", str(folder), "--no-deps", str(target), "-q"] - run_suppress_output(cmd, stop_print_on_fail=True) - return next(iter(folder.iterdir())) - - -def run_suppress_output(cmd: list[str], stop_print_on_fail: bool = False) -> int: # noqa: FBT002 - process = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - encoding="utf-8", - ) - out, err = process.communicate() - if stop_print_on_fail and process.returncode != 0: - print(f"exit with {process.returncode} of {' '.join(quote(i) for i in cmd)}", file=sys.stdout) # noqa: T201 - if out: - print(out, file=sys.stdout) # noqa: T201 - if err: - print(err, file=sys.stderr) # noqa: T201 - raise SystemExit(process.returncode) - return process.returncode - - -def get_wheels_for_support_versions(folder: Path) -> dict[str, Any]: - downloader = WheelDownloader(folder / "wheel-store") - downloader.run(HERE.parent, VERSIONS) - packages = defaultdict(lambda: defaultdict(lambda: defaultdict(WheelForVersion))) - for version, collected in downloader.collected.items(): - for pkg, platform_to_wheel in collected.items(): - name = Requirement(pkg).name - for platform, wheel in platform_to_wheel.items(): - pl = platform or "==any" - wheel_versions = packages[name][pl][wheel.name] - wheel_versions.versions.append(version) - wheel_versions.wheel = wheel - for name, p_w_v in packages.items(): - for platform, w_v in p_w_v.items(): - print(f"{name} - {platform}") # noqa: T201 - for wheel, wheel_versions in w_v.items(): - print(f"{' '.join(wheel_versions.versions)} of {wheel} (use {wheel_versions.wheel})") # noqa: T201 - return packages - - -class WheelForVersion: - def __init__(self, wheel: Path | None = None, versions: list[str] | None = None) -> None: - self.wheel = wheel - self.versions = versions or [] - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.wheel!r}, {self.versions!r})" - - -if __name__ == "__main__": - main() diff --git a/tasks/release.py b/tasks/release.py deleted file mode 100644 index f876d5cb8..000000000 --- a/tasks/release.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Handles creating a release.""" - -from __future__ import annotations - -from pathlib import Path -from subprocess import call, check_call - -from git import Commit, Remote, Repo, TagReference -from packaging.version import Version - -ROOT_SRC_DIR = Path(__file__).resolve().parents[1] -CHANGELOG_DIR = ROOT_SRC_DIR / "docs" / "changelog" - - -def main(version_str: str, *, push: bool) -> None: - repo = Repo(str(ROOT_SRC_DIR)) - if repo.is_dirty(): - msg = "Current repository is dirty. Please commit any changes and try again." - raise RuntimeError(msg) - remote = get_remote(repo) - remote.fetch() - version = resolve_version(version_str, repo) - print(f"releasing {version}") # noqa: T201 - release_commit = release_changelog(repo, version) - tag = tag_release_commit(release_commit, repo, version) - if push: - print("push release commit") # noqa: T201 - repo.git.push(remote.name, "HEAD:main") - print("push release tag") # noqa: T201 - repo.git.push(remote.name, tag) - print("All done! โœจ ๐Ÿฐ โœจ") # noqa: T201 - - -def resolve_version(version_str: str, repo: Repo) -> Version: - if version_str not in {"auto", "major", "minor", "patch"}: - return Version(version_str) - latest_tag = repo.git.describe("--tags", "--abbrev=0") - parts = [int(x) for x in latest_tag.split(".")] - if version_str == "major": - parts = [parts[0] + 1, 0, 0] - elif version_str == "minor": - parts = [parts[0], parts[1] + 1, 0] - elif version_str == "patch": - parts[2] += 1 - elif any(CHANGELOG_DIR.glob("*.feature.rst")) or any(CHANGELOG_DIR.glob("*.removal.rst")): - parts = [parts[0], parts[1] + 1, 0] - else: - parts[2] += 1 - return Version(".".join(str(p) for p in parts)) - - -def get_remote(repo: Repo) -> Remote: - upstream_remote = "pypa/virtualenv" - urls = set() - for remote in repo.remotes: - for url in remote.urls: - if url.rstrip(".git").endswith(upstream_remote): - return remote - urls.add(url) - msg = f"could not find {upstream_remote} remote, has {urls}" - raise RuntimeError(msg) - - -def release_changelog(repo: Repo, version: Version) -> Commit: - print("generate release commit") # noqa: T201 - check_call(["towncrier", "build", "--yes", "--version", version.public], cwd=str(ROOT_SRC_DIR)) # noqa: S607 - call(["pre-commit", "run", "--all-files"], cwd=str(ROOT_SRC_DIR)) # noqa: S607 - repo.git.add(".") - check_call(["pre-commit", "run", "--all-files"], cwd=str(ROOT_SRC_DIR)) # noqa: S607 - return repo.index.commit(f"release {version}") - - -def tag_release_commit(release_commit: Commit, repo: Repo, version: Version) -> TagReference: - print("tag release commit") # noqa: T201 - existing_tags = [x.name for x in repo.tags] - if version in existing_tags: - print(f"delete existing tag {version}") # noqa: T201 - repo.delete_tag(version) - print(f"create tag {version}") # noqa: T201 - return repo.create_tag(version, ref=release_commit, force=True) - - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(prog="release") - parser.add_argument("--version", default="auto") - parser.add_argument("--no-push", action="store_true") - options = parser.parse_args() - main(options.version, push=not options.no_push) diff --git a/tasks/update_embedded.py b/tasks/update_embedded.py deleted file mode 100644 index 99b4ffa4c..000000000 --- a/tasks/update_embedded.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Helper script to rebuild virtualenv.py from virtualenv_support.""" - -from __future__ import annotations - -import codecs -import locale -import os -import re -from typing import TYPE_CHECKING, NoReturn -from zlib import crc32 as _crc32 - -if TYPE_CHECKING: - from pathlib import Path - - -def crc32(data: str) -> int: - """Python version idempotent.""" - return _crc32(data.encode()) & 0xFFFFFFFF - - -here = os.path.realpath(os.path.dirname(__file__)) -script = os.path.realpath(os.path.join(here, "..", "src", "virtualenv.py")) - -gzip = codecs.lookup("zlib") -b64 = codecs.lookup("base64") - -file_regex = re.compile(r'# file (.*?)\n([a-zA-Z][a-zA-Z0-9_]+) = convert\(\n {4}"""\n(.*?)"""\n\)', re.DOTALL) -file_template = '# file {filename}\n{variable} = convert(\n """\n{data}"""\n)' - - -def rebuild(script_path: Path) -> None: - encoding = ( - locale.getencoding() if hasattr(locale, "getencoding") else locale.getpreferredencoding(do_setlocale=False) - ) - with script_path.open(encoding=encoding) as current_fh: - script_content = current_fh.read() - script_parts = [] - match_end = 0 - next_match = None - count, did_update = 0, False - for count, next_match in enumerate(file_regex.finditer(script_content)): # noqa: B007 - script_parts += [script_content[match_end : next_match.start()]] - match_end = next_match.end() - filename, variable_name, previous_encoded = next_match.group(1), next_match.group(2), next_match.group(3) - differ, content = handle_file(next_match.group(0), filename, variable_name, previous_encoded) - script_parts.append(content) - if differ: - did_update = True - - script_parts += [script_content[match_end:]] - new_content = "".join(script_parts) - - report(1 if not count or did_update else 0, new_content, next_match, script_content, script_path) - - -def handle_file(previous_content: str, filename: str, variable_name: str, previous_encoded: str) -> tuple[bool, str]: - print(f"Found file {filename}") # noqa: T201 - current_path = os.path.realpath(os.path.join(here, "..", "src", "virtualenv_embedded", filename)) - _, file_type = os.path.splitext(current_path) - keep_line_ending = file_type == ".bat" - with open(current_path, encoding="utf-8", newline="" if keep_line_ending else None) as current_fh: - current_text = current_fh.read() - current_crc = crc32(current_text) - current_encoded = b64.encode(gzip.encode(current_text.encode())[0])[0].decode() - if current_encoded == previous_encoded: - print(f" File up to date (crc: {current_crc:08x})") # noqa: T201 - return False, previous_content - # Else: content has changed - previous_text = gzip.decode(b64.decode(previous_encoded.encode())[0])[0].decode() - previous_crc = crc32(previous_text) - print(f" Content changed (crc: {previous_crc:08x} -> {current_crc:08x})") # noqa: T201 - new_part = file_template.format(filename=filename, variable=variable_name, data=current_encoded) - return True, new_part - - -def report(exit_code: int, new: str, next_match: re.Match[str] | None, current: str, script_path: Path) -> NoReturn: - if new != current: - print("Content updated; overwriting... ", end="") # noqa: T201 - script_path.write_bytes(new) - print("done.") # noqa: T201 - else: - print("No changes in content") # noqa: T201 - if next_match is None: - print("No variables were matched/found") # noqa: T201 - raise SystemExit(exit_code) - - -if __name__ == "__main__": - rebuild(script) diff --git a/tasks/upgrade_wheels.py b/tasks/upgrade_wheels.py deleted file mode 100644 index 881a18e66..000000000 --- a/tasks/upgrade_wheels.py +++ /dev/null @@ -1,157 +0,0 @@ -"""Helper script to rebuild virtualenv_support. Downloads the wheel files using pip.""" - -from __future__ import annotations - -import os -import shutil -import subprocess -import sys -from collections import OrderedDict, defaultdict -from pathlib import Path -from tempfile import TemporaryDirectory -from textwrap import dedent -from threading import Thread -from typing import NoReturn - -STRICT = "UPGRADE_ADVISORY" not in os.environ - -BUNDLED = ["pip", "setuptools", "wheel"] -SUPPORT = [(3, i) for i in range(8, 16)] -DEST = Path(__file__).resolve().parents[1] / "src" / "virtualenv" / "seed" / "wheels" / "embed" - - -def download(ver: str, dest: str, package: str) -> None: - subprocess.call( - [ - sys.executable, - "-W", - "ignore::EncodingWarning", - "-m", - "pip", - "--disable-pip-version-check", - "download", - "--no-cache-dir", - "--only-binary=:all:", - "--python-version", - ver, - "-d", - dest, - package, - ], - ) - - -def run() -> NoReturn: # noqa: C901, PLR0912 - old_batch = {i.name for i in DEST.iterdir() if i.suffix == ".whl"} - with TemporaryDirectory() as temp: - temp_path = Path(temp) - folders = {} - targets = [] - for support in SUPPORT: - support_ver = ".".join(str(i) for i in support) - into = temp_path / support_ver - into.mkdir() - folders[into] = support_ver - for package in BUNDLED: - if package == "wheel" and support >= (3, 9): - continue - thread = Thread(target=download, args=(support_ver, str(into), package)) - targets.append(thread) - thread.start() - for thread in targets: - thread.join() - new_batch = {i.name: i for f in folders for i in Path(f).iterdir()} - - new_packages = new_batch.keys() - old_batch - remove_packages = old_batch - new_batch.keys() - - for package in remove_packages: - (DEST / package).unlink() - for package in new_packages: - shutil.copy2(str(new_batch[package]), DEST / package) - - added = collect_package_versions(new_packages) - removed = collect_package_versions(remove_packages) - outcome = (1 if STRICT else 0) if (added or removed) else 0 - print(f"Outcome {outcome} added {added} removed {removed}") # noqa: T201 - lines = ["Upgrade embedded wheels:", ""] - for key, versions in added.items(): - text = f"* {key} to {fmt_version(versions)}" - if key in removed: - rem = ", ".join(f"``{i}``" for i in removed[key]) - text += f" from {rem}" - del removed[key] - lines.append(text) - for key, versions in removed.items(): - lines.append(f"Removed {key} of {fmt_version(versions)}") - lines.append("") - changelog = "\n".join(lines) - print(changelog) # noqa: T201 - if len(lines) >= 4: # noqa: PLR2004 - (Path(__file__).parents[1] / "docs" / "changelog" / "u.bugfix.rst").write_text(changelog, encoding="utf-8") - support_table = OrderedDict((".".join(str(j) for j in i), []) for i in SUPPORT) - for package in sorted(new_batch.keys()): - for folder, version in sorted(folders.items()): - if (folder / package).exists(): - support_table[version].append(package) - support_table = {k: OrderedDict((i.split("-")[0], i) for i in v) for k, v in support_table.items()} - nl = "\n" - bundle = "".join( - f"\n {v!r}: {{{nl}{''.join(f' {p!r}: {f!r},{nl}' for p, f in line.items())} }}," - for v, line in support_table.items() - ) - msg = dedent( - f""" - from __future__ import annotations - - from pathlib import Path - - from virtualenv.seed.wheels.util import Wheel - - BUNDLE_FOLDER = Path(__file__).absolute().parent - BUNDLE_SUPPORT = {{ {bundle} }} - MAX = {next(iter(support_table.keys()))!r} - - - def get_embed_wheel(distribution: str, for_py_version: str) -> Wheel | None: - mapping = BUNDLE_SUPPORT.get(for_py_version, {{}}) or BUNDLE_SUPPORT[MAX] - wheel_file = mapping.get(distribution) - if wheel_file is None: - return None - path = BUNDLE_FOLDER / wheel_file - return Wheel.from_path(path) - - __all__ = [ - "BUNDLE_FOLDER", - "BUNDLE_SUPPORT", - "MAX", - "get_embed_wheel", - ] - - """, - ) - dest_target = DEST / "__init__.py" - dest_target.write_text(msg, encoding="utf-8") - subprocess.run([sys.executable, "-m", "ruff", "check", str(dest_target), "--fix", "--unsafe-fixes"]) - subprocess.run([sys.executable, "-m", "ruff", "format", str(dest_target), "--preview"]) - - raise SystemExit(outcome) - - -def fmt_version(versions: list[str]) -> str: - return ", ".join(f"``{v}``" for v in versions) - - -def collect_package_versions(new_packages: set[str]) -> dict[str, list[str]]: - result = defaultdict(list) - for package in new_packages: - split = package.split("-") - if len(split) < 2: # noqa: PLR2004 - raise ValueError(package) - key, version = split[0:2] - result[key].append(version) - return result - - -if __name__ == "__main__": - run() diff --git a/src/virtualenv/config/__init__.py b/tests/__init__.py similarity index 100% rename from src/virtualenv/config/__init__.py rename to tests/__init__.py diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 3ec4147c5..000000000 --- a/tests/conftest.py +++ /dev/null @@ -1,371 +0,0 @@ -from __future__ import annotations - -import logging -import os -import shutil -import sys -from contextlib import contextmanager -from functools import partial -from pathlib import Path -from typing import ClassVar - -import pytest -from python_discovery import PythonInfo - -from virtualenv.app_data import AppDataDiskFolder -from virtualenv.info import IS_GRAALPY, IS_PYPY, IS_RUSTPYTHON, IS_WIN, fs_supports_symlink -from virtualenv.report import LOGGER - - -def pytest_addoption(parser) -> None: - parser.addoption("--int", action="store_true", default=False, help="run integration tests") - parser.addoption("--skip-slow", action="store_true", default=False, help="skip slow tests") - - -def pytest_configure(config) -> None: - """Ensure randomly is called before we re-order""" - manager = config.pluginmanager - - order = manager.hook.pytest_collection_modifyitems.get_hookimpls() - dest = next((i for i, p in enumerate(order) if p.plugin is manager.getplugin("randomly")), None) - if dest is not None: - from_pos = next(i for i, p in enumerate(order) if p.plugin is manager.getplugin(__file__)) - temp = order[dest] - order[dest] = order[from_pos] - order[from_pos] = temp - - -def pytest_collection_modifyitems(config, items) -> None: - int_location = os.path.join("tests", "integration", "").rstrip() - if len(items) == 1: - return - - items.sort(key=lambda i: 2 if i.location[0].startswith(int_location) else (1 if "slow" in i.keywords else 0)) - - if not config.getoption("--int"): - for item in items: - if item.location[0].startswith(int_location): - item.add_marker(pytest.mark.skip(reason="need --int option to run")) - - if config.getoption("--skip-slow"): - for item in items: - if "slow" in [mark.name for mark in item.iter_markers()]: - item.add_marker(pytest.mark.skip(reason="skipped because --skip-slow was passed")) - - -@pytest.fixture(scope="session") -def has_symlink_support(): - return fs_supports_symlink() - - -@pytest.fixture(scope="session") -def link_folder(has_symlink_support): - if has_symlink_support: - return os.symlink - if sys.platform == "win32": - # on Windows junctions may be used instead - import _winapi # noqa: PLC0415 - - return getattr(_winapi, "CreateJunction", None) - return None - - -@pytest.fixture(scope="session") -def link_file(has_symlink_support): - if has_symlink_support: - return os.symlink - return None - - -@pytest.fixture(scope="session") -def link(link_folder, link_file): - def _link(src, dest): - clean = dest.unlink - s_dest = str(dest) - s_src = str(src) - if src.is_dir(): - if link_folder: - link_folder(s_src, s_dest) - else: - shutil.copytree(s_src, s_dest) - clean = partial(shutil.rmtree, str(dest)) - elif link_file: - link_file(s_src, s_dest) - else: - shutil.copy2(s_src, s_dest) - return clean - - return _link - - -@pytest.fixture(autouse=True) -def _ensure_logging_stable(): - logger_level = LOGGER.level - handlers = list(LOGGER.handlers) - filelock_logger = logging.getLogger("filelock") - fl_level = filelock_logger.level - yield - filelock_logger.setLevel(fl_level) - for handler in LOGGER.handlers: - LOGGER.removeHandler(handler) - for handler in handlers: - LOGGER.addHandler(handler) - LOGGER.setLevel(logger_level) - - -@pytest.fixture(autouse=True) -def _check_cwd_not_changed_by_test(): - old = os.getcwd() - yield - new = os.getcwd() - if old != new: - pytest.fail(f"tests changed cwd: {old!r} => {new!r}") - - -@pytest.fixture(autouse=True) -def _ensure_py_info_cache_empty(session_app_data): - PythonInfo.clear_cache(session_app_data) - yield - PythonInfo.clear_cache(session_app_data) - - -@contextmanager -def change_os_environ(key, value): - env_var = key - previous = os.environ.get(env_var, None) - os.environ[env_var] = value - try: - yield - finally: - if previous is not None: - os.environ[env_var] = previous - - -@pytest.fixture(autouse=True, scope="session") -def _ignore_global_config(tmp_path_factory): - filename = str(tmp_path_factory.mktemp("folder") / "virtualenv-test-suite.ini") - with change_os_environ("VIRTUALENV_CONFIG_FILE", filename): - yield - - -@pytest.fixture(autouse=True) -def _check_os_environ_stable(): - old = os.environ.copy() - # ensure we don't inherit parent env variables - to_clean = {k for k in os.environ if k.startswith(("VIRTUALENV_", "TOX_")) or "VIRTUAL_ENV" in k} - cleaned = {k: os.environ[k] for k in os.environ} - override = { - "VIRTUALENV_NO_PERIODIC_UPDATE": "1", - "VIRTUALENV_NO_DOWNLOAD": "1", - } - for key, value in override.items(): - os.environ[str(key)] = str(value) - is_exception = False - try: - yield - except BaseException: - is_exception = True - raise - finally: - try: - for key in override: - del os.environ[str(key)] - if is_exception is False: - new = os.environ - extra = {k: new[k] for k in set(new) - set(old)} - miss = {k: old[k] for k in set(old) - set(new) - to_clean} - diff = { - f"{k} = {old[k]} vs {new[k]}" - for k in set(old) & set(new) - if old[k] != new[k] and not k.startswith("PYTEST_") - } - if extra or miss or diff: - msg = "tests changed environ" - if extra: - msg += f" extra {extra}" - if miss: - msg += f" miss {miss}" - if diff: - msg += f" diff {diff}" - pytest.fail(msg) - finally: - os.environ.update(cleaned) - - -COV_ENV_VAR = "COVERAGE_PROCESS_START" -COVERAGE_RUN = os.environ.get(str(COV_ENV_VAR)) - - -@pytest.fixture(autouse=True) -def coverage_env(monkeypatch, link, request): - """Enable coverage report collection on the created virtual environments by injecting the coverage project""" - if COVERAGE_RUN and "_no_coverage" not in request.fixturenames: - # we inject right after creation, we cannot collect coverage on site.py - used for helper scripts, such as debug - from virtualenv import run # noqa: PLC0415 - - def _session_via_cli(args, options, setup_logging, env=None): - session = prev_run(args, options, setup_logging, env) - old_run = session.creator.run - - def create_run(): - result = old_run() - obj["cov"] = EnableCoverage(link) - obj["cov"].__enter__(session.creator) # noqa: PLC2801 - return result - - monkeypatch.setattr(session.creator, "run", create_run) - return session - - obj = {"cov": None} - prev_run = run.session_via_cli - monkeypatch.setattr(run, "session_via_cli", _session_via_cli) - - def finish() -> None: - cov = obj["cov"] - obj["cov"] = None - cov.__exit__(None, None, None) - - yield finish - if obj["cov"]: - finish() - - else: - - def finish() -> None: - pass - - yield finish - - -# _no_coverage tells coverage_env to disable coverage injection for _no_coverage user. -@pytest.fixture -def _no_coverage() -> None: - pass - - -if COVERAGE_RUN: - import coverage - - class EnableCoverage: - _COV_FILE: ClassVar[Path] = Path(coverage.__file__) - _ROOT_COV_FILES_AND_FOLDERS: ClassVar[list[Path]] = [ - i for i in _COV_FILE.parents[1].iterdir() if i.name.startswith("coverage") - ] - - def __init__(self, link) -> None: - self.link = link - self.targets = [] - - def __enter__(self, creator): # noqa: PLE0302 - site_packages = creator.purelib - for entry in self._ROOT_COV_FILES_AND_FOLDERS: - target = site_packages / entry.name - if not target.exists(): - clean = self.link(entry, target) - self.targets.append((target, clean)) - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - for target, clean in self.targets: - if target.exists(): - clean() - assert self._COV_FILE.exists() - - -@pytest.fixture(scope="session") -def is_inside_ci(): - return bool(os.environ.get("CI_RUN")) - - -@pytest.fixture(scope="session") -def special_char_name(): - base = "'\";&&e-$ รจั€ั‚๐Ÿš’โ™žไธญ็‰‡-j" - if IS_WIN: - # get rid of invalid characters on Windows - base = base.replace('"', "") - base = base.replace(";", "") - # workaround for pypy3 https://bitbucket.org/pypy/pypy/issues/3147/venv-non-ascii-support-windows - encoding = "ascii" if IS_WIN else sys.getfilesystemencoding() - # let's not include characters that the file system cannot encode) - result = "" - for char in base: - try: - trip = char.encode(encoding, errors="strict").decode(encoding) - if char == trip: - result += char - except ValueError: # noqa: PERF203 - continue - assert result - return result - - -@pytest.fixture -def special_name_dir(tmp_path, special_char_name): - return Path(str(tmp_path)) / special_char_name - - -@pytest.fixture(scope="session") -def current_creators(session_app_data): - from virtualenv.run.plugin.creators import CreatorSelector # noqa: PLC0415 - - return CreatorSelector.for_interpreter(PythonInfo.current_system(session_app_data)) - - -@pytest.fixture(scope="session") -def current_fastest(current_creators): - return "builtin" if "builtin" in current_creators.key_to_class else next(iter(current_creators.key_to_class)) - - -@pytest.fixture(scope="session") -def session_app_data(tmp_path_factory): - temp_folder = tmp_path_factory.mktemp("session-app-data") - app_data = AppDataDiskFolder(folder=str(temp_folder)) - with change_env_var("VIRTUALENV_OVERRIDE_APP_DATA", str(app_data.lock.path)): - yield app_data - - -@contextmanager -def change_env_var(key, value): - """Temporarily change an environment variable. :param key: the key of the env var :param value: the value of the env var""" - already_set = key in os.environ - prev_value = os.environ.get(key) - os.environ[key] = value - try: - yield - finally: - if already_set: - os.environ[key] = prev_value - else: - del os.environ[key] # pragma: no cover - - -@pytest.fixture -def temp_app_data(monkeypatch, tmp_path): - app_data = tmp_path / "app-data" - monkeypatch.setenv("VIRTUALENV_OVERRIDE_APP_DATA", str(app_data)) - return app_data - - -@pytest.fixture(scope="session") -def for_py_version() -> str: - return f"{sys.version_info.major}.{sys.version_info.minor}" - - -@pytest.fixture -def _skip_if_test_in_system(session_app_data) -> None: - current = PythonInfo.current(session_app_data) - if current.system_executable is not None: - pytest.skip("test not valid if run under system") - - -if IS_PYPY or IS_GRAALPY or IS_RUSTPYTHON: - - @pytest.fixture - def time_freeze(freezer): - return freezer.move_to - -else: - - @pytest.fixture - def time_freeze(time_machine): - return lambda s: time_machine.move_to(s, tick=False) diff --git a/tests/integration/test_cachedir_tag.py b/tests/integration/test_cachedir_tag.py deleted file mode 100644 index b3b6a1dfe..000000000 --- a/tests/integration/test_cachedir_tag.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations - -import shutil -import sys -from subprocess import check_output, run -from typing import TYPE_CHECKING - -import pytest - -from virtualenv import cli_run - -if TYPE_CHECKING: - from pathlib import Path - -# gtar => gnu-tar on macOS -TAR = next((target for target in ("gtar", "tar") if shutil.which(target)), None) - - -def compatible_is_tar_present() -> bool: - return TAR and "--exclude-caches" in check_output(args=[TAR, "--help"], text=True, encoding="utf-8") - - -@pytest.mark.skipif(sys.platform == "win32", reason="Windows does not have tar") -@pytest.mark.skipif(not compatible_is_tar_present(), reason="Compatible tar is not installed") -def test_cachedir_tag_ignored_by_tag(tmp_path: Path) -> None: - venv = tmp_path / ".venv" - cli_run(["--activators", "", "--without-pip", str(venv)]) - - args = [TAR, "--create", "--file", "/dev/null", "--exclude-caches", "--verbose", venv.name] - tar_result = run(args=args, capture_output=True, text=True, encoding="utf-8", cwd=tmp_path) - assert tar_result.stdout == ".venv/\n.venv/CACHEDIR.TAG\n" - assert tar_result.stderr == f"{TAR}: .venv/: contains a cache directory tag CACHEDIR.TAG; contents not dumped\n" diff --git a/tests/integration/test_race_condition_simulation.py b/tests/integration/test_race_condition_simulation.py deleted file mode 100644 index 8e4067b54..000000000 --- a/tests/integration/test_race_condition_simulation.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import annotations - -import importlib.util -import shutil -import sys -from pathlib import Path - - -def test_race_condition_simulation(tmp_path) -> None: - """Test that simulates the race condition described in the issue. - - This test creates a temporary directory with _virtualenv.py and _virtualenv.pth, then simulates the scenario where: - - One process imports and uses the _virtualenv module (simulating marimo) - Another process overwrites the - _virtualenv.py file (simulating uv venv) - - The test verifies that no NameError is raised for _DISTUTILS_PATCH. - - """ - # Create the _virtualenv.py file - virtualenv_file = tmp_path / "_virtualenv.py" - source_file = Path(__file__).parents[2] / "src" / "virtualenv" / "create" / "via_global_ref" / "_virtualenv.py" - - shutil.copy(source_file, virtualenv_file) - - # Create the _virtualenv.pth file - pth_file = tmp_path / "_virtualenv.pth" - pth_file.write_text("import _virtualenv", encoding="utf-8") - - # Simulate the race condition by repeatedly importing - errors = [] - for _ in range(5): - # Try to import it - sys.path.insert(0, str(tmp_path)) - try: - if "_virtualenv" in sys.modules: - del sys.modules["_virtualenv"] - - import _virtualenv # noqa: F401, PLC0415 - - # Try to trigger find_spec - try: - importlib.util.find_spec("distutils.dist") - except NameError as e: - if "_DISTUTILS_PATCH" in str(e): - errors.append(str(e)) - finally: - if str(tmp_path) in sys.path: - sys.path.remove(str(tmp_path)) - - assert not errors, f"Race condition detected: {errors}" diff --git a/tests/integration/test_run_int.py b/tests/integration/test_run_int.py deleted file mode 100644 index f09348873..000000000 --- a/tests/integration/test_run_int.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pytest - -from virtualenv import cli_run -from virtualenv.info import IS_PYPY -from virtualenv.util.subprocess import run_cmd - -if TYPE_CHECKING: - from pathlib import Path - - -@pytest.mark.graalpy -@pytest.mark.skipif(IS_PYPY, reason="setuptools distutils patching does not work") -def test_app_data_pinning(tmp_path: Path) -> None: - version = "23.1" - result = cli_run([str(tmp_path), "--pip", version, "--activators", "", "--seeder", "app-data"]) - code, out, _ = run_cmd([str(result.creator.script("pip")), "list", "--disable-pip-version-check"]) - assert not code - for line in out.splitlines(): - parts = line.split() - if parts and parts[0] == "pip": - assert parts[1] == version - break - else: - assert not out diff --git a/tests/integration/test_zipapp.py b/tests/integration/test_zipapp.py deleted file mode 100644 index d7af9f664..000000000 --- a/tests/integration/test_zipapp.py +++ /dev/null @@ -1,119 +0,0 @@ -from __future__ import annotations - -import shutil -import subprocess -from contextlib import suppress -from pathlib import Path - -import pytest -from python_discovery import PythonInfo - -from virtualenv.info import fs_supports_symlink -from virtualenv.run import cli_run - -HERE = Path(__file__).parent -CURRENT = PythonInfo.current_system() - - -@pytest.fixture(scope="session") -def zipapp_build_env(tmp_path_factory): - create_env_path = None - if CURRENT.implementation not in {"PyPy", "GraalVM"}: - exe = CURRENT.executable # guaranteed to contain a recent enough pip (tox.ini) - else: - create_env_path = tmp_path_factory.mktemp("zipapp-create-env") - exe, found = None, False - # prefer CPython as builder as pypy is slow - for impl in ["cpython", ""]: - for threaded in ["", "t"]: - for version in range(11, 6, -1): - with suppress(Exception): - # create a virtual environment which is also guaranteed to contain a recent enough pip (bundled) - session = cli_run( - [ - "-vvv", - "-p", - f"{impl}3.{version}{threaded}", - "--activators", - "", - str(create_env_path), - "--no-download", - "--no-periodic-update", - ], - ) - exe = str(session.creator.exe) - found = True - break - if found: - break - if found: - break - else: - msg = "could not find a python to build zipapp" - raise RuntimeError(msg) - cmd = [str(Path(exe).parent / "pip"), "install", "pip>=23", "packaging>=23"] - subprocess.run(cmd, check=True, timeout=300) - yield exe - if create_env_path is not None: - shutil.rmtree(str(create_env_path)) - - -@pytest.fixture(scope="session") -def zipapp(zipapp_build_env, tmp_path_factory): - into = tmp_path_factory.mktemp("zipapp") - path = HERE.parent.parent / "tasks" / "make_zipapp.py" - filename = into / "virtualenv.pyz" - cmd = [zipapp_build_env, str(path), "--dest", str(filename)] - subprocess.run(cmd, check=True, timeout=300) - yield filename - shutil.rmtree(str(into)) - - -@pytest.fixture(scope="session") -def zipapp_test_env(tmp_path_factory): - base_path = tmp_path_factory.mktemp("zipapp-test") - session = cli_run(["-v", "--activators", "", "--without-pip", str(base_path / "env"), "--no-periodic-update"]) - yield session.creator.exe - shutil.rmtree(str(base_path)) - - -@pytest.fixture -def call_zipapp(zipapp, tmp_path, zipapp_test_env, temp_app_data): # noqa: ARG001 - def _run(*args) -> None: - cmd = [str(zipapp_test_env), str(zipapp), "-vv", str(tmp_path / "env"), *list(args)] - subprocess.run(cmd, check=True, timeout=120) - - return _run - - -@pytest.fixture -def call_zipapp_symlink(zipapp, tmp_path, zipapp_test_env, temp_app_data): # noqa: ARG001 - def _run(*args) -> None: - symlinked = zipapp.parent / "symlinked_virtualenv.pyz" - symlinked.symlink_to(str(zipapp)) - cmd = [str(zipapp_test_env), str(symlinked), "-vv", str(tmp_path / "env"), *list(args)] - subprocess.run(cmd, check=True, timeout=120) - - return _run - - -@pytest.mark.timeout(600) -@pytest.mark.skipif(not fs_supports_symlink(), reason="symlink not supported") -def test_zipapp_in_symlink(capsys, call_zipapp_symlink) -> None: - call_zipapp_symlink("--reset-app-data") - _out, err = capsys.readouterr() - assert not err - - -@pytest.mark.timeout(600) -def test_zipapp_help(call_zipapp, capsys) -> None: - call_zipapp("-h") - _out, err = capsys.readouterr() - assert not err - - -@pytest.mark.timeout(600) -@pytest.mark.slow -@pytest.mark.parametrize("seeder", ["app-data", "pip"]) -def test_zipapp_create(call_zipapp, seeder) -> None: - call_zipapp("--seeder", seeder) diff --git a/tests/test_activate.sh b/tests/test_activate.sh new file mode 100755 index 000000000..e27727386 --- /dev/null +++ b/tests/test_activate.sh @@ -0,0 +1,96 @@ +#!/bin/sh + +set -u + +ROOT="$(dirname $0)/.." +VIRTUALENV="${ROOT}/virtualenv.py" +TESTENV="/tmp/test_virtualenv_activate.venv" + +rm -rf ${TESTENV} + +echo "$0: Creating virtualenv ${TESTENV}..." 1>&2 + +${VIRTUALENV} ${TESTENV} | tee ${ROOT}/tests/test_activate_output.actual +if ! diff ${ROOT}/tests/test_activate_output.expected ${ROOT}/tests/test_activate_output.actual; then + echo "$0: Failed to get expected output from ${VIRTUALENV}!" 1>&2 + exit 1 +fi + +echo "$0: Created virtualenv ${TESTENV}." 1>&2 + +echo "$0: Activating ${TESTENV}..." 1>&2 +. ${TESTENV}/bin/activate +echo "$0: Activated ${TESTENV}." 1>&2 + +echo "$0: Checking value of \$VIRTUAL_ENV..." 1>&2 + +if [ "$VIRTUAL_ENV" != "${TESTENV}" ]; then + echo "$0: Expected \$VIRTUAL_ENV to be set to \"${TESTENV}\"; actual value: \"${VIRTUAL_ENV}\"!" 1>&2 + exit 2 +fi + +echo "$0: \$VIRTUAL_ENV = \"${VIRTUAL_ENV}\" -- OK." 1>&2 + +echo "$0: Checking output of \$(which python)..." 1>&2 + +if [ "$(which python)" != "${TESTENV}/bin/python" ]; then + echo "$0: Expected \$(which python) to return \"${TESTENV}/bin/python\"; actual value: \"$(which python)\"!" 1>&2 + exit 3 +fi + +echo "$0: Output of \$(which python) is OK." 1>&2 + +echo "$0: Checking output of \$(which pip)..." 1>&2 + +if [ "$(which pip)" != "${TESTENV}/bin/pip" ]; then + echo "$0: Expected \$(which pip) to return \"${TESTENV}/bin/pip\"; actual value: \"$(which pip)\"!" 1>&2 + exit 4 +fi + +echo "$0: Output of \$(which pip) is OK." 1>&2 + +echo "$0: Checking output of \$(which easy_install)..." 1>&2 + +if [ "$(which easy_install)" != "${TESTENV}/bin/easy_install" ]; then + echo "$0: Expected \$(which easy_install) to return \"${TESTENV}/bin/easy_install\"; actual value: \"$(which easy_install)\"!" 1>&2 + exit 5 +fi + +echo "$0: Output of \$(which easy_install) is OK." 1>&2 + +echo "$0: Executing a simple Python program..." 1>&2 + +TESTENV=${TESTENV} python <<__END__ +import os, sys + +expected_site_packages = os.path.join(os.environ['TESTENV'], 'lib','python%s' % sys.version[:3], 'site-packages') +site_packages = os.path.join(os.environ['VIRTUAL_ENV'], 'lib', 'python%s' % sys.version[:3], 'site-packages') + +assert site_packages == expected_site_packages, 'site_packages did not have expected value; actual value: %r' % site_packages + +open(os.path.join(site_packages, 'pydoc_test.py'), 'w').write('"""This is pydoc_test.py"""\n') +__END__ + +if [ $? -ne 0 ]; then + echo "$0: Python script failed!" 1>&2 + exit 6 +fi + +echo "$0: Execution of a simple Python program -- OK." 1>&2 + +echo "$0: Testing pydoc..." 1>&2 + +if ! PAGER=cat pydoc pydoc_test | grep 'This is pydoc_test.py' > /dev/null; then + echo "$0: pydoc test failed!" 1>&2 + exit 7 +fi + +echo "$0: pydoc is OK." 1>&2 + +echo "$0: Deactivating ${TESTENV}..." 1>&2 +deactivate +echo "$0: Deactivated ${TESTENV}." 1>&2 +echo "$0: OK!" 1>&2 + +rm -rf ${TESTENV} + diff --git a/tests/test_activate_output.expected b/tests/test_activate_output.expected new file mode 100644 index 000000000..d49469feb --- /dev/null +++ b/tests/test_activate_output.expected @@ -0,0 +1,2 @@ +New python executable in /tmp/test_virtualenv_activate.venv/bin/python +Installing setuptools, pip, wheel...done. diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py new file mode 100644 index 000000000..9682ef003 --- /dev/null +++ b/tests/test_cmdline.py @@ -0,0 +1,44 @@ +import sys +import subprocess +import virtualenv +import pytest + +VIRTUALENV_SCRIPT = virtualenv.__file__ + +def test_commandline_basic(tmpdir): + """Simple command line usage should work""" + subprocess.check_call([ + sys.executable, + VIRTUALENV_SCRIPT, + str(tmpdir.join('venv')) + ]) + +def test_commandline_explicit_interp(tmpdir): + """Specifying the Python interpreter should work""" + subprocess.check_call([ + sys.executable, + VIRTUALENV_SCRIPT, + '-p', sys.executable, + str(tmpdir.join('venv')) + ]) + +# The registry lookups to support the abbreviated "-p 3.5" form of specifying +# a Python interpreter on Windows don't seem to work with Python 3.5. The +# registry layout is not well documented, and it's not clear that the feature +# is sufficiently widely used to be worth fixing. +# See https://github.com/pypa/virtualenv/issues/864 +@pytest.mark.skipif("sys.platform == 'win32' and sys.version_info[:2] >= (3,5)") +def test_commandline_abbrev_interp(tmpdir): + """Specifying abbreviated forms of the Python interpreter should work""" + if sys.platform == 'win32': + fmt = '%s.%s' + else: + fmt = 'python%s.%s' + abbrev = fmt % (sys.version_info[0], sys.version_info[1]) + subprocess.check_call([ + sys.executable, + VIRTUALENV_SCRIPT, + '-p', abbrev, + str(tmpdir.join('venv')) + ]) + diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py new file mode 100644 index 000000000..ce45ede4a --- /dev/null +++ b/tests/test_virtualenv.py @@ -0,0 +1,141 @@ +import virtualenv +import optparse +import os +import shutil +import sys +import tempfile +import pytest +import platform # noqa + +from mock import patch, Mock + + +def test_version(): + """Should have a version string""" + assert virtualenv.virtualenv_version, "Should have version" + + +@patch('os.path.exists') +def test_resolve_interpreter_with_absolute_path(mock_exists): + """Should return absolute path if given and exists""" + mock_exists.return_value = True + virtualenv.is_executable = Mock(return_value=True) + test_abs_path = os.path.abspath("/usr/bin/python53") + + exe = virtualenv.resolve_interpreter(test_abs_path) + + assert exe == test_abs_path, "Absolute path should return as is" + + mock_exists.assert_called_with(test_abs_path) + virtualenv.is_executable.assert_called_with(test_abs_path) + + +@patch('os.path.exists') +def test_resolve_interpreter_with_nonexistent_interpreter(mock_exists): + """Should SystemExit with an nonexistent python interpreter path""" + mock_exists.return_value = False + + with pytest.raises(SystemExit): + virtualenv.resolve_interpreter("/usr/bin/python53") + + mock_exists.assert_called_with("/usr/bin/python53") + + +@patch('os.path.exists') +def test_resolve_interpreter_with_invalid_interpreter(mock_exists): + """Should exit when with absolute path if not exists""" + mock_exists.return_value = True + virtualenv.is_executable = Mock(return_value=False) + invalid = os.path.abspath("/usr/bin/pyt_hon53") + + with pytest.raises(SystemExit): + virtualenv.resolve_interpreter(invalid) + + mock_exists.assert_called_with(invalid) + virtualenv.is_executable.assert_called_with(invalid) + + +def test_activate_after_future_statements(): + """Should insert activation line after last future statement""" + script = [ + '#!/usr/bin/env python', + 'from __future__ import with_statement', + 'from __future__ import print_function', + 'print("Hello, world!")' + ] + assert virtualenv.relative_script(script) == [ + '#!/usr/bin/env python', + 'from __future__ import with_statement', + 'from __future__ import print_function', + '', + "import os; activate_this=os.path.join(os.path.dirname(os.path.realpath(__file__)), 'activate_this.py'); exec(compile(open(activate_this).read(), activate_this, 'exec'), dict(__file__=activate_this)); del os, activate_this", + '', + 'print("Hello, world!")' + ] + + +def test_cop_update_defaults_with_store_false(): + """store_false options need reverted logic""" + class MyConfigOptionParser(virtualenv.ConfigOptionParser): + def __init__(self, *args, **kwargs): + self.config = virtualenv.ConfigParser.RawConfigParser() + self.files = [] + optparse.OptionParser.__init__(self, *args, **kwargs) + + def get_environ_vars(self, prefix='VIRTUALENV_'): + yield ("no_site_packages", "1") + + cop = MyConfigOptionParser() + cop.add_option( + '--no-site-packages', + dest='system_site_packages', + action='store_false', + help="Don't give access to the global site-packages dir to the " + "virtual environment (default)") + + defaults = {} + cop.update_defaults(defaults) + assert defaults == {'system_site_packages': 0} + + +def test_install_python_bin(): + """Should create the right python executables and links""" + tmp_virtualenv = tempfile.mkdtemp() + try: + home_dir, lib_dir, inc_dir, bin_dir = \ + virtualenv.path_locations(tmp_virtualenv) + virtualenv.install_python(home_dir, lib_dir, inc_dir, bin_dir, False, + False) + + if virtualenv.is_win: + required_executables = ['python.exe', 'pythonw.exe'] + else: + py_exe_no_version = 'python' + py_exe_version_major = 'python%s' % sys.version_info[0] + py_exe_version_major_minor = 'python%s.%s' % ( + sys.version_info[0], sys.version_info[1]) + required_executables = [py_exe_no_version, py_exe_version_major, + py_exe_version_major_minor] + + for pth in required_executables: + assert os.path.exists(os.path.join(bin_dir, pth)), \ + ("%s should exist in bin_dir" % pth) + finally: + shutil.rmtree(tmp_virtualenv) + + +@pytest.mark.skipif("platform.python_implementation() == 'PyPy'") +def test_always_copy_option(): + """Should be no symlinks in directory tree""" + tmp_virtualenv = tempfile.mkdtemp() + ve_path = os.path.join(tmp_virtualenv, 'venv') + try: + virtualenv.create_environment(ve_path, symlink=False) + + for root, dirs, files in os.walk(tmp_virtualenv): + for f in files + dirs: + full_name = os.path.join(root, f) + assert not os.path.islink(full_name), "%s should not be a" \ + " symlink (to %s)" % (full_name, os.readlink(full_name)) + finally: + shutil.rmtree(tmp_virtualenv) diff --git a/tests/types.py b/tests/types.py deleted file mode 100644 index 0466460b2..000000000 --- a/tests/types.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import annotations - -from typing import Protocol - - -class _VersionInfo(Protocol): - major: int - minor: int - - -class Interpreter(Protocol): - prefix: str - system_prefix: str - system_executable: str - free_threaded: bool - version_info: _VersionInfo - sysconfig_vars: dict[str, object] - - -class MakeInterpreter(Protocol): - def __call__( - self, - sysconfig_vars: dict[str, object] | None = ..., - prefix: str = ..., - free_threaded: bool = ..., - version_info: tuple[int, ...] = ..., - ) -> Interpreter: ... diff --git a/tests/unit/activation/conftest.py b/tests/unit/activation/conftest.py deleted file mode 100644 index 804be848a..000000000 --- a/tests/unit/activation/conftest.py +++ /dev/null @@ -1,260 +0,0 @@ -from __future__ import annotations - -import os -import re -import subprocess -import sys -from os.path import dirname, normcase -from pathlib import Path -from subprocess import Popen - -import pytest - -from virtualenv.run import cli_run - - -class ActivationTester: - def __init__(self, of_class, session, cmd, activate_script, extension) -> None: - self.of_class = of_class - self._creator = session.creator - self._version_cmd = [cmd, "--version"] - self._invoke_script = [cmd] - self.activate_script = activate_script - self.extension = extension - self.activate_cmd = "source" - self.deactivate = "deactivate" - self.pydoc_call = "pydoc -w pydoc_test" - self.script_encoding = "utf-8" - self._version = None - self.unix_line_ending = True - - def get_version(self, raise_on_fail): - if self._version is None: - # locally we disable, so that contributors don't need to have everything setup - try: - process = Popen( - self._version_cmd, - universal_newlines=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - encoding="utf-8", - ) - out, err = process.communicate() - except Exception as exception: - self._version = exception - if raise_on_fail: - raise - return RuntimeError(f"{self} is not available due {exception}") - else: - result = out or err - self._version = result - return result - return self._version - - def __repr__(self) -> str: - return ( - f"{self.__class__.__name__}(\nversion={self._version!r},\ncreator={self._creator},\n" - f"interpreter={self._creator.interpreter})" - ) - - def __call__(self, monkeypatch, tmp_path): - activate_script = self._creator.bin_dir / self.activate_script - - # check line endings are correct type - script_content = activate_script.read_bytes() - for line in script_content.split(b"\n")[:-1]: - if self.unix_line_ending: - assert line == b"" or line[-1] != 13, script_content.decode("utf-8") - else: - assert line[-1] == 13, script_content.decode("utf-8") - - test_script = self._generate_test_script(activate_script, tmp_path) - monkeypatch.chdir(tmp_path) - - monkeypatch.delenv("VIRTUAL_ENV", raising=False) - invoke, env = [*self._invoke_script, str(test_script)], self.env(tmp_path) - - try: - process = Popen(invoke, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) - raw_, _ = process.communicate(timeout=120) - except subprocess.TimeoutExpired: - process.kill() - process.communicate() - pytest.fail(f"Activation script timed out: {invoke}") - except subprocess.CalledProcessError as exception: - output = exception.output + exception.stderr - assert not exception.returncode, output # noqa: PT017 - return None - else: - raw = raw_.decode(errors="replace") - assert process.returncode == 0, raw - - out = re.sub(r"pydev debugger: process \d+ is connecting\n\n", "", raw, flags=re.MULTILINE).strip().splitlines() - self.assert_output(out, raw, tmp_path) - return env, activate_script - - def non_source_activate(self, activate_script): - return [*self._invoke_script, str(activate_script)] - - def env(self, tmp_path): # noqa: ARG002 - env = os.environ.copy() - # add the current python executable folder to the path so we already have another python on the path - # also keep the path so the shells (fish, bash, etc can be discovered) - env["PYTHONIOENCODING"] = "utf-8" - env["PATH"] = os.pathsep.join([dirname(sys.executable), *env.get("PATH", "").split(os.pathsep)]) - # clear up some environment variables so they don't affect the tests - for key in [k for k in env if k.startswith(("_OLD", "VIRTUALENV_"))]: - del env[key] - return env - - def _generate_test_script(self, activate_script, tmp_path): - commands = self._get_test_lines(activate_script) - script = os.linesep.join(commands) - test_script = tmp_path / f"script.{self.extension}" - with test_script.open("wb") as file_handler: - file_handler.write(script.encode(self.script_encoding)) - return test_script - - def _get_test_lines(self, activate_script): - return [ - self.print_python_exe(), - self.print_os_env_var("VIRTUAL_ENV"), - self.print_os_env_var("VIRTUAL_ENV_PROMPT"), - self.activate_call(activate_script), - self.print_python_exe(), - self.print_os_env_var("VIRTUAL_ENV"), - self.print_os_env_var("VIRTUAL_ENV_PROMPT"), - self.print_prompt(), - # \\ loads documentation from the virtualenv site packages - self.pydoc_call, - self.deactivate, - self.print_python_exe(), - self.print_os_env_var("VIRTUAL_ENV"), - self.print_os_env_var("VIRTUAL_ENV_PROMPT"), - "", # just finish with an empty new line - ] - - def assert_output(self, out, raw, tmp_path) -> None: - """Compare _get_test_lines() with the expected values.""" - assert out[0], raw - assert out[1] == "None", raw - assert out[2] == "None", raw - # self.activate_call(activate_script) runs at this point - python_exe = self._creator.exe.parent / os.path.basename(sys.executable) - assert self.norm_path(out[3]) == self.norm_path(python_exe), raw - assert self.norm_path(out[4]) == self.norm_path(self._creator.dest).replace("\\\\", "\\"), raw - assert out[5] == self._creator.env_name - # Some attempts to test the prompt output print more than 1 line. - # So we need to check if the prompt exists on any of them. - prompt_text = f"({self._creator.env_name}) " - assert any(prompt_text in line for line in out[6:-4]), raw - - assert out[-4] == "wrote pydoc_test.html", raw - content = tmp_path / "pydoc_test.html" - assert content.exists(), raw - # post deactivation, same as before - assert out[-3] == out[0], raw - assert out[-2] == "None", raw - assert out[-1] == "None", raw - - def quote(self, s): - return self.of_class.quote(s) - - def python_cmd(self, cmd) -> str: - return f"{os.path.basename(sys.executable)} -c {self.quote(cmd)}" - - def print_python_exe(self): - return self.python_cmd("import sys; print(sys.executable)") - - def print_os_env_var(self, var): - val = f'"{var}"' - return self.python_cmd(f"import os; import sys; v = os.environ.get({val}); print(v)") - - def print_prompt(self): - return NotImplemented - - def activate_call(self, script): - cmd = self.quote(str(self.activate_cmd)) - scr = self.quote(str(script)) - return f"{cmd} {scr}".strip() - - @staticmethod - def norm_path(path): - # python may return Windows short paths, normalize - if not isinstance(path, Path): - path = Path(path) - path = str(path.resolve()) - if sys.platform != "win32": - result = path - else: - from ctypes import create_unicode_buffer, windll # noqa: PLC0415 - - buffer_cont = create_unicode_buffer(256) - get_long_path_name = windll.kernel32.GetLongPathNameW - get_long_path_name(str(path), buffer_cont, 256) - result = buffer_cont.value or path - return normcase(result) - - -class RaiseOnNonSourceCall(ActivationTester): - def __init__( # noqa: PLR0913 - self, - of_class, - session, - cmd, - activate_script, - extension, - non_source_fail_message, - ) -> None: - super().__init__(of_class, session, cmd, activate_script, extension) - self.non_source_fail_message = non_source_fail_message - - def __call__(self, monkeypatch, tmp_path): - env, activate_script = super().__call__(monkeypatch, tmp_path) - process = Popen( - self.non_source_activate(activate_script), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=env, - ) - _out, err_ = process.communicate() - err = err_.decode("utf-8") - assert process.returncode - assert self.non_source_fail_message in err - - -@pytest.fixture(scope="session") -def activation_tester_class(): - return ActivationTester - - -@pytest.fixture(scope="session") -def raise_on_non_source_class(): - return RaiseOnNonSourceCall - - -@pytest.fixture(scope="session", params=[True, False], ids=["with_prompt", "no_prompt"]) -def activation_python(request, tmp_path_factory, special_char_name, current_fastest): - dest = os.path.join(str(tmp_path_factory.mktemp("activation-tester-env")), special_char_name) - cmd = ["--without-pip", dest, "--creator", current_fastest, "-vv", "--no-periodic-update"] - # `params` is accessed here. https://docs.pytest.org/en/stable/reference/reference.html#pytest-fixture - if request.param: - cmd += ["--prompt", special_char_name] - session = cli_run(cmd) - pydoc_test = session.creator.purelib / "pydoc_test.py" - pydoc_test.write_text('"""This is pydoc_test.py"""', encoding="utf-8") - return session - - -@pytest.fixture -def activation_tester(activation_python, monkeypatch, tmp_path, is_inside_ci): - def _tester(tester_class): - tester = tester_class(activation_python) - if not tester.of_class.supports(activation_python.creator.interpreter): - pytest.skip(f"{tester.of_class.__name__} not supported") - version = tester.get_version(raise_on_fail=is_inside_ci) - if not isinstance(version, str): - pytest.skip(reason=str(version)) - return tester(monkeypatch, tmp_path) - - return _tester diff --git a/tests/unit/activation/test_activation_support.py b/tests/unit/activation/test_activation_support.py deleted file mode 100644 index a0e2a756b..000000000 --- a/tests/unit/activation/test_activation_support.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import annotations - -from argparse import Namespace - -import pytest -from python_discovery import PythonInfo - -from virtualenv.activation import ( - BashActivator, - BatchActivator, - CShellActivator, - FishActivator, - PowerShellActivator, - PythonActivator, -) - - -@pytest.mark.parametrize( - "activator_class", - [BatchActivator, PowerShellActivator, PythonActivator, BashActivator, FishActivator], -) -def test_activator_support_windows(mocker, activator_class) -> None: - activator = activator_class(Namespace(prompt=None)) - - interpreter = mocker.Mock(spec=PythonInfo) - interpreter.os = "nt" - assert activator.supports(interpreter) - - -@pytest.mark.parametrize("activator_class", [CShellActivator]) -def test_activator_no_support_windows(mocker, activator_class) -> None: - activator = activator_class(Namespace(prompt=None)) - - interpreter = mocker.Mock(spec=PythonInfo) - interpreter.os = "nt" - assert not activator.supports(interpreter) - - -@pytest.mark.parametrize( - "activator_class", - [BashActivator, CShellActivator, FishActivator, PowerShellActivator, PythonActivator], -) -def test_activator_support_posix(mocker, activator_class) -> None: - activator = activator_class(Namespace(prompt=None)) - interpreter = mocker.Mock(spec=PythonInfo) - interpreter.os = "posix" - assert activator.supports(interpreter) - - -@pytest.mark.parametrize("activator_class", [BatchActivator]) -def test_activator_no_support_posix(mocker, activator_class) -> None: - activator = activator_class(Namespace(prompt=None)) - interpreter = mocker.Mock(spec=PythonInfo) - interpreter.os = "posix" - assert not activator.supports(interpreter) diff --git a/tests/unit/activation/test_activator.py b/tests/unit/activation/test_activator.py deleted file mode 100644 index 21be96d06..000000000 --- a/tests/unit/activation/test_activator.py +++ /dev/null @@ -1,21 +0,0 @@ -from __future__ import annotations - -from argparse import Namespace - -import pytest - -from virtualenv.activation.activator import Activator - - -@pytest.mark.graalpy -def test_activator_prompt_cwd(monkeypatch, tmp_path) -> None: - class FakeActivator(Activator): - def generate(self, creator): - raise NotImplementedError - - cwd = tmp_path / "magic" - cwd.mkdir() - monkeypatch.chdir(cwd) - - activator = FakeActivator(Namespace(prompt=".")) - assert activator.flag_prompt == "magic" diff --git a/tests/unit/activation/test_bash.py b/tests/unit/activation/test_bash.py deleted file mode 100644 index 7a1bd35f0..000000000 --- a/tests/unit/activation/test_bash.py +++ /dev/null @@ -1,126 +0,0 @@ -from __future__ import annotations - -import shutil -import subprocess -from argparse import Namespace - -import pytest - -from virtualenv.activation import BashActivator -from virtualenv.info import IS_WIN -from virtualenv.run import cli_run - - -@pytest.mark.skipif(IS_WIN, reason="Github Actions ships with WSL bash") -@pytest.mark.parametrize( - ("tcl_lib", "tk_lib", "present"), - [ - ("/path/to/tcl", "/path/to/tk", True), - (None, None, False), - ], -) -def test_bash_tkinter_generation(tmp_path, tcl_lib, tk_lib, present) -> None: - # GIVEN - class MockInterpreter: - pass - - interpreter = MockInterpreter() - interpreter.tcl_lib = tcl_lib - interpreter.tk_lib = tk_lib - - class MockCreator: - def __init__(self, dest) -> None: - self.dest = dest - self.bin_dir = dest / "bin" - self.bin_dir.mkdir() - self.interpreter = interpreter - self.pyenv_cfg = {} - self.env_name = "my-env" - - creator = MockCreator(tmp_path) - options = Namespace(prompt=None) - activator = BashActivator(options) - - # WHEN - activator.generate(creator) - content = (creator.bin_dir / "activate").read_text(encoding="utf-8") - - # THEN - # The teardown logic is always present in deactivate() - assert "unset _OLD_VIRTUAL_TCL_LIBRARY" in content - assert "unset _OLD_VIRTUAL_TK_LIBRARY" in content - assert "unset _OLD_PKG_CONFIG_PATH" in content - - # PKG_CONFIG_PATH is always set - assert '_OLD_PKG_CONFIG_PATH="${PKG_CONFIG_PATH:-}"' in content - assert 'PKG_CONFIG_PATH="${VIRTUAL_ENV}/lib/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}"' in content - assert "export PKG_CONFIG_PATH" in content - assert 'PKG_CONFIG_PATH="$_OLD_PKG_CONFIG_PATH"' in content - - if present: - assert 'if [ /path/to/tcl != "" ]; then' in content - assert "TCL_LIBRARY=/path/to/tcl" in content - assert "export TCL_LIBRARY" in content - - assert 'if [ /path/to/tk != "" ]; then' in content - assert "TK_LIBRARY=/path/to/tk" in content - assert "export TK_LIBRARY" in content - else: - # When not present, the if condition is false, so the block is not executed - assert "if [ '' != \"\" ]; then" in content, content - assert "TCL_LIBRARY=''" in content - # The export is inside the if, so this is fine - assert "export TCL_LIBRARY" in content - - -@pytest.mark.skipif(IS_WIN, reason="Github Actions ships with WSL bash") -def test_bash_activate_relocation_resolves_virtual_env(tmp_path, current_fastest) -> None: - original = tmp_path / "original" - cli_run([ - "--without-pip", - str(original), - "--creator", - current_fastest, - "--no-periodic-update", - "--activators", - "bash", - ]) - relocated = tmp_path / "relocated" - shutil.move(original, relocated) - - work_dir = tmp_path / "workdir" - work_dir.mkdir() - activate_script = relocated / "bin" / "activate" - result = subprocess.run( - ["bash", "-c", f'source "{activate_script}" 2>/dev/null && echo "$VIRTUAL_ENV"'], - capture_output=True, - text=True, - cwd=str(work_dir), - ) - assert result.returncode == 0 - assert result.stdout.strip() == str(relocated) - - -@pytest.mark.skipif(IS_WIN, reason="Github Actions ships with WSL bash") -@pytest.mark.parametrize("hashing_enabled", [True, False]) -def test_bash(raise_on_non_source_class, hashing_enabled, activation_tester) -> None: - class Bash(raise_on_non_source_class): - def __init__(self, session) -> None: - super().__init__( - BashActivator, - session, - "bash", - "activate", - "sh", - "You must source this script: $ source ", - ) - self.deactivate += " || exit 1" - self._invoke_script.append("-h" if hashing_enabled else "+h") - - def activate_call(self, script): - return super().activate_call(script) + " || exit 1" - - def print_prompt(self): - return self.print_os_env_var("PS1") - - activation_tester(Bash) diff --git a/tests/unit/activation/test_batch.py b/tests/unit/activation/test_batch.py deleted file mode 100644 index 7882e7f45..000000000 --- a/tests/unit/activation/test_batch.py +++ /dev/null @@ -1,163 +0,0 @@ -from __future__ import annotations - -from argparse import Namespace - -import pytest - -from virtualenv.activation import BatchActivator - - -def test_batch_pydoc_bat_quoting(tmp_path) -> None: - """Test that pydoc.bat properly quotes python.exe path to handle spaces.""" - - # GIVEN: A mock interpreter - class MockInterpreter: - os = "nt" - tcl_lib = None - tk_lib = None - - class MockCreator: - def __init__(self, dest) -> None: - self.dest = dest - self.bin_dir = dest / "Scripts" - self.bin_dir.mkdir(parents=True) - self.interpreter = MockInterpreter() - self.pyenv_cfg = {} - self.env_name = "test-env" - - creator = MockCreator(tmp_path) - options = Namespace(prompt=None) - activator = BatchActivator(options) - - # WHEN: Generate activation scripts - activator.generate(creator) - - # THEN: pydoc.bat should quote python.exe to handle paths with spaces - pydoc_content = (creator.bin_dir / "pydoc.bat").read_text(encoding="utf-8") - - # The python.exe should be quoted to handle paths with spaces like "C:\Program Files\Python39\python.exe" - assert '"python.exe"' in pydoc_content, f"python.exe should be quoted in pydoc.bat. Content:\n{pydoc_content}" - - -@pytest.mark.parametrize( - ("tcl_lib", "tk_lib", "present"), - [ - ("C:\\tcl", "C:\\tk", True), - (None, None, False), - ], -) -def test_batch_tkinter_generation(tmp_path, tcl_lib, tk_lib, present) -> None: - # GIVEN - class MockInterpreter: - os = "nt" - - interpreter = MockInterpreter() - interpreter.tcl_lib = tcl_lib - interpreter.tk_lib = tk_lib - - class MockCreator: - def __init__(self, dest) -> None: - self.dest = dest - self.bin_dir = dest / "bin" - self.bin_dir.mkdir() - self.interpreter = interpreter - self.pyenv_cfg = {} - self.env_name = "my-env" - - creator = MockCreator(tmp_path) - options = Namespace(prompt=None) - activator = BatchActivator(options) - - # WHEN - activator.generate(creator) - activate_content = (creator.bin_dir / "activate.bat").read_text(encoding="utf-8") - deactivate_content = (creator.bin_dir / "deactivate.bat").read_text(encoding="utf-8") - - # THEN - # PKG_CONFIG_PATH is always set - assert '@if defined PKG_CONFIG_PATH @set "_OLD_PKG_CONFIG_PATH=%PKG_CONFIG_PATH%"' in activate_content - assert '@set "PKG_CONFIG_PATH=%VIRTUAL_ENV%\\lib\\pkgconfig;%PKG_CONFIG_PATH%"' in activate_content - assert '@if defined _OLD_PKG_CONFIG_PATH @set "PKG_CONFIG_PATH=%_OLD_PKG_CONFIG_PATH%"' in deactivate_content - assert "@if not defined _OLD_PKG_CONFIG_PATH @set PKG_CONFIG_PATH=" in deactivate_content - assert "@set _OLD_PKG_CONFIG_PATH=" in deactivate_content - - if present: - assert '@if NOT "C:\\tcl"=="" @set "TCL_LIBRARY=C:\\tcl"' in activate_content - assert '@if NOT "C:\\tk"=="" @set "TK_LIBRARY=C:\\tk"' in activate_content - assert "if defined _OLD_VIRTUAL_TCL_LIBRARY" in deactivate_content - assert "if defined _OLD_VIRTUAL_TK_LIBRARY" in deactivate_content - else: - assert '@if NOT ""=="" @set "TCL_LIBRARY="' in activate_content - assert '@if NOT ""=="" @set "TK_LIBRARY="' in activate_content - - -@pytest.mark.usefixtures("activation_python") -def test_batch(activation_tester_class, activation_tester, tmp_path) -> None: - version_script = tmp_path / "version.bat" - version_script.write_text("ver", encoding="utf-8") - - class Batch(activation_tester_class): - def __init__(self, session) -> None: - super().__init__(BatchActivator, session, None, "activate.bat", "bat") - self._version_cmd = [str(version_script)] - self._invoke_script = [] - self.deactivate = "call deactivate" - self.activate_cmd = "call" - self.pydoc_call = f"call {self.pydoc_call}" - self.unix_line_ending = False - - def _get_test_lines(self, activate_script): - return ["@echo off", *super()._get_test_lines(activate_script)] - - def quote(self, s): - if '"' in s or " " in s: - text = s.replace('"', r"\"") - return f'"{text}"' - return s - - def print_prompt(self) -> str: - return 'echo "%PROMPT%"' - - activation_tester(Batch) - - -@pytest.mark.usefixtures("activation_python") -def test_batch_output(activation_tester_class, activation_tester, tmp_path) -> None: - version_script = tmp_path / "version.bat" - version_script.write_text("ver", encoding="utf-8") - - class Batch(activation_tester_class): - def __init__(self, session) -> None: - super().__init__(BatchActivator, session, None, "activate.bat", "bat") - self._version_cmd = [str(version_script)] - self._invoke_script = [] - self.deactivate = "call deactivate" - self.activate_cmd = "call" - self.pydoc_call = f"call {self.pydoc_call}" - self.unix_line_ending = False - - def _get_test_lines(self, activate_script): - """Build intermediary script which will be then called. In the script just activate environment, call echo to get current echo setting, and then deactivate. This ensures that echo setting is preserved and no unwanted output appears.""" - intermediary_script_path = str(tmp_path / "intermediary.bat") - activate_script_quoted = self.quote(str(activate_script)) - return [ - "@echo on", - f"@echo @call {activate_script_quoted} > {intermediary_script_path}", - f"@echo @echo >> {intermediary_script_path}", - f"@echo @deactivate >> {intermediary_script_path}", - f"@call {intermediary_script_path}", - ] - - def assert_output(self, out, raw, tmp_path) -> None: # noqa: ARG002 - assert out[0] == "ECHO is on.", raw - - def quote(self, s): - if '"' in s or " " in s: - text = s.replace('"', r"\"") - return f'"{text}"' - return s - - def print_prompt(self) -> str: - return 'echo "%PROMPT%"' - - activation_tester(Batch) diff --git a/tests/unit/activation/test_csh.py b/tests/unit/activation/test_csh.py deleted file mode 100644 index 891ee08d8..000000000 --- a/tests/unit/activation/test_csh.py +++ /dev/null @@ -1,81 +0,0 @@ -from __future__ import annotations - -import sys -from argparse import Namespace -from shutil import which -from subprocess import check_output - -import pytest -from packaging.version import Version - -from virtualenv.activation import CShellActivator - - -@pytest.mark.parametrize( - ("tcl_lib", "tk_lib", "present"), - [ - ("/path/to/tcl", "/path/to/tk", True), - (None, None, False), - ], -) -def test_cshell_tkinter_generation(tmp_path, tcl_lib, tk_lib, present) -> None: - # GIVEN - class MockInterpreter: - pass - - interpreter = MockInterpreter() - interpreter.tcl_lib = tcl_lib - interpreter.tk_lib = tk_lib - - class MockCreator: - def __init__(self, dest) -> None: - self.dest = dest - self.bin_dir = dest / "bin" - self.bin_dir.mkdir() - self.interpreter = interpreter - self.pyenv_cfg = {} - self.env_name = "my-env" - - creator = MockCreator(tmp_path) - options = Namespace(prompt=None) - activator = CShellActivator(options) - - # WHEN - activator.generate(creator) - content = (creator.bin_dir / "activate.csh").read_text(encoding="utf-8") - - # PKG_CONFIG_PATH is always set - assert "test $?_OLD_PKG_CONFIG_PATH != 0" in content - assert 'set _OLD_PKG_CONFIG_PATH="$PKG_CONFIG_PATH"' in content - assert 'setenv PKG_CONFIG_PATH "${VIRTUAL_ENV}/lib/pkgconfig:${PKG_CONFIG_PATH}"' in content - assert 'setenv PKG_CONFIG_PATH "${VIRTUAL_ENV}/lib/pkgconfig"' in content - assert 'setenv PKG_CONFIG_PATH "$_OLD_PKG_CONFIG_PATH:q"' in content - assert "unset _OLD_PKG_CONFIG_PATH" in content - - if present: - assert "test $?_OLD_VIRTUAL_TCL_LIBRARY != 0" in content - assert "test $?_OLD_VIRTUAL_TK_LIBRARY != 0" in content - assert "setenv TCL_LIBRARY /path/to/tcl" in content - assert "setenv TK_LIBRARY /path/to/tk" in content - else: - assert "setenv TCL_LIBRARY ''" in content - - -def test_csh(activation_tester_class, activation_tester) -> None: - exe = f"tcsh{'.exe' if sys.platform == 'win32' else ''}" - if which(exe): - version_text = check_output([exe, "--version"], text=True, encoding="utf-8") - version = Version(version_text.split(" ")[1]) - if version >= Version("6.24.14"): - pytest.skip("https://github.com/tcsh-org/tcsh/issues/117") - - class Csh(activation_tester_class): - def __init__(self, session) -> None: - super().__init__(CShellActivator, session, "csh", "activate.csh", "csh") - - def print_prompt(self) -> str: - # Original csh doesn't print the last newline, - # breaking the test; hence the trailing echo. - return "echo 'source \"$VIRTUAL_ENV/bin/activate.csh\"; echo $prompt' | csh -i ; echo" - - activation_tester(Csh) diff --git a/tests/unit/activation/test_fish.py b/tests/unit/activation/test_fish.py deleted file mode 100644 index 4d28b7866..000000000 --- a/tests/unit/activation/test_fish.py +++ /dev/null @@ -1,124 +0,0 @@ -from __future__ import annotations - -import os -import sys -from argparse import Namespace - -import pytest - -from virtualenv.activation import FishActivator -from virtualenv.info import IS_WIN - - -@pytest.mark.parametrize( - ("tcl_lib", "tk_lib", "present"), - [ - ("/path/to/tcl", "/path/to/tk", True), - (None, None, False), - ], -) -def test_fish_tkinter_generation(tmp_path, tcl_lib, tk_lib, present) -> None: - # GIVEN - class MockInterpreter: - pass - - interpreter = MockInterpreter() - interpreter.tcl_lib = tcl_lib - interpreter.tk_lib = tk_lib - - class MockCreator: - def __init__(self, dest) -> None: - self.dest = dest - self.bin_dir = dest / "bin" - self.bin_dir.mkdir() - self.interpreter = interpreter - self.pyenv_cfg = {} - self.env_name = "my-env" - - creator = MockCreator(tmp_path) - options = Namespace(prompt=None) - activator = FishActivator(options) - - # WHEN - activator.generate(creator) - content = (creator.bin_dir / "activate.fish").read_text(encoding="utf-8") - - # THEN - # PKG_CONFIG_PATH is always set - assert 'set -gx _OLD_PKG_CONFIG_PATH "$PKG_CONFIG_PATH"' in content - assert 'set -gx PKG_CONFIG_PATH "$VIRTUAL_ENV/lib/pkgconfig:$PKG_CONFIG_PATH"' in content - assert "set -e _OLD_PKG_CONFIG_PATH" in content - - if present: - assert "set -gx TCL_LIBRARY '/path/to/tcl'" in content - assert "set -gx TK_LIBRARY '/path/to/tk'" in content - else: - assert "if test -n ''\n if set -q TCL_LIBRARY;" in content - assert "if test -n ''\n if set -q TK_LIBRARY;" in content - - -@pytest.mark.skipif(IS_WIN, reason="we have not setup fish in CI yet") -def test_fish(activation_tester_class, activation_tester, monkeypatch, tmp_path) -> None: - monkeypatch.setenv("HOME", str(tmp_path)) - fish_conf_dir = tmp_path / ".config" / "fish" - fish_conf_dir.mkdir(parents=True) - (fish_conf_dir / "config.fish").write_text("", encoding="utf-8") - - class Fish(activation_tester_class): - def __init__(self, session) -> None: - super().__init__(FishActivator, session, "fish", "activate.fish", "fish") - - def print_prompt(self) -> str: - return "fish_prompt" - - def _get_test_lines(self, activate_script): - return [ - self.print_python_exe(), - self.print_os_env_var("VIRTUAL_ENV"), - self.print_os_env_var("VIRTUAL_ENV_PROMPT"), - self.print_os_env_var("PATH"), - self.activate_call(activate_script), - self.print_python_exe(), - self.print_os_env_var("VIRTUAL_ENV"), - self.print_os_env_var("VIRTUAL_ENV_PROMPT"), - self.print_os_env_var("PATH"), - self.print_prompt(), - # \\ loads documentation from the virtualenv site packages - self.pydoc_call, - self.deactivate, - self.print_python_exe(), - self.print_os_env_var("VIRTUAL_ENV"), - self.print_os_env_var("VIRTUAL_ENV_PROMPT"), - self.print_os_env_var("PATH"), - "", # just finish with an empty new line - ] - - def assert_output(self, out, raw, _) -> None: - """Compare _get_test_lines() with the expected values.""" - assert out[0], raw - assert out[1] == "None", raw - assert out[2] == "None", raw - # self.activate_call(activate_script) runs at this point - expected = self._creator.exe.parent / os.path.basename(sys.executable) - assert self.norm_path(out[4]) == self.norm_path(expected), raw - assert self.norm_path(out[5]) == self.norm_path(self._creator.dest).replace("\\\\", "\\"), raw - assert out[6] == self._creator.env_name - # Some attempts to test the prompt output print more than 1 line. - # So we need to check if the prompt exists on any of them. - prompt_text = f"({self._creator.env_name}) " - assert any(prompt_text in line for line in out[7:-5]), raw - - assert out[-5] == "wrote pydoc_test.html", raw - content = tmp_path / "pydoc_test.html" - assert content.exists(), raw - # post deactivation, same as before - assert out[-4] == out[0], raw - assert out[-3] == "None", raw - assert out[-2] == "None", raw - - # Check that the PATH is restored - assert out[3] == out[13], raw - # Check that PATH changed after activation - assert out[3] != out[8], raw - - activation_tester(Fish) diff --git a/tests/unit/activation/test_nushell.py b/tests/unit/activation/test_nushell.py deleted file mode 100644 index 284de52fe..000000000 --- a/tests/unit/activation/test_nushell.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import annotations - -from argparse import Namespace -from shutil import which - -from virtualenv.activation import NushellActivator -from virtualenv.info import IS_WIN - - -def test_nushell_tkinter_generation(tmp_path) -> None: - # GIVEN - class MockInterpreter: - pass - - interpreter = MockInterpreter() - interpreter.tcl_lib = "/path/to/tcl" - interpreter.tk_lib = "/path/to/tk" - quoted_tcl_path = NushellActivator.quote(interpreter.tcl_lib) - quoted_tk_path = NushellActivator.quote(interpreter.tk_lib) - - class MockCreator: - def __init__(self, dest) -> None: - self.dest = dest - self.bin_dir = dest / "bin" - self.bin_dir.mkdir() - self.interpreter = interpreter - self.pyenv_cfg = {} - self.env_name = "my-env" - - creator = MockCreator(tmp_path) - options = Namespace(prompt=None) - activator = NushellActivator(options) - - # WHEN - activator.generate(creator) - content = (creator.bin_dir / "activate.nu").read_text(encoding="utf-8") - - # THEN - # PKG_CONFIG_PATH is always set - assert "let old_pkg_config_path = if (has-env 'PKG_CONFIG_PATH')" in content - assert "let new_pkg_config_path = " in content - assert "PKG_CONFIG_PATH: $new_pkg_config_path" in content - - expected_tcl = f"let $new_env = $new_env | insert TCL_LIBRARY {quoted_tcl_path}" - expected_tk = f"let $new_env = $new_env | insert TK_LIBRARY {quoted_tk_path}" - - assert expected_tcl in content - assert expected_tk in content - - -def test_nushell(activation_tester_class, activation_tester) -> None: - class Nushell(activation_tester_class): - def __init__(self, session) -> None: - super().__init__(NushellActivator, session, which("nu"), "activate.nu", "nu") - - self.activate_cmd = "overlay use" - self.unix_line_ending = not IS_WIN - - def print_prompt(self) -> str: - return r"print $env.VIRTUAL_PREFIX" - - def activate_call(self, script): - # Commands are called without quotes in Nushell - cmd = self.activate_cmd - scr = self.quote(str(script)) - return f"{cmd} {scr}".strip() - - activation_tester(Nushell) diff --git a/tests/unit/activation/test_powershell.py b/tests/unit/activation/test_powershell.py deleted file mode 100644 index 8c95705a7..000000000 --- a/tests/unit/activation/test_powershell.py +++ /dev/null @@ -1,134 +0,0 @@ -from __future__ import annotations - -import sys -from argparse import Namespace - -import pytest - -from virtualenv.activation import PowerShellActivator - - -def test_powershell_pydoc_call_operator(tmp_path) -> None: - """Test that PowerShell pydoc function uses call operator to handle spaces in python path.""" - - # GIVEN: A mock interpreter - class MockInterpreter: - os = "nt" - tcl_lib = None - tk_lib = None - - class MockCreator: - def __init__(self, dest) -> None: - self.dest = dest - self.bin_dir = dest / "Scripts" - self.bin_dir.mkdir(parents=True) - self.interpreter = MockInterpreter() - self.pyenv_cfg = {} - self.env_name = "test-env" - - creator = MockCreator(tmp_path) - options = Namespace(prompt=None) - activator = PowerShellActivator(options) - - # WHEN: Generate activation scripts - activator.generate(creator) - - # THEN: pydoc function should use call operator & to handle paths with spaces - activate_content = (creator.bin_dir / "activate.ps1").read_text(encoding="utf-8-sig") - - # The pydoc function should use & call operator to handle paths with spaces - assert "& python -m pydoc" in activate_content, ( - f"pydoc function should use & call operator. Content:\n{activate_content}" - ) - - -@pytest.mark.parametrize( - ("tcl_lib", "tk_lib", "present"), - [ - ("C:\\tcl", "C:\\tk", True), - (None, None, False), - ], -) -def test_powershell_tkinter_generation(tmp_path, tcl_lib, tk_lib, present) -> None: - # GIVEN - class MockInterpreter: - os = "nt" - - interpreter = MockInterpreter() - interpreter.tcl_lib = tcl_lib - interpreter.tk_lib = tk_lib - - class MockCreator: - def __init__(self, dest) -> None: - self.dest = dest - self.bin_dir = dest / "bin" - self.bin_dir.mkdir() - self.interpreter = interpreter - self.pyenv_cfg = {} - self.env_name = "my-env" - - creator = MockCreator(tmp_path) - options = Namespace(prompt=None) - activator = PowerShellActivator(options) - - # WHEN - activator.generate(creator) - content = (creator.bin_dir / "activate.ps1").read_text(encoding="utf-8-sig") - - # THEN - # PKG_CONFIG_PATH is always set - assert "New-Variable -Scope global -Name _OLD_PKG_CONFIG_PATH" in content - assert '$env:PKG_CONFIG_PATH = "$env:VIRTUAL_ENV\\lib\\pkgconfig;$env:PKG_CONFIG_PATH"' in content - assert "if (Test-Path variable:_OLD_PKG_CONFIG_PATH)" in content - assert "$env:PKG_CONFIG_PATH = $variable:_OLD_PKG_CONFIG_PATH" in content - assert 'Remove-Variable "_OLD_PKG_CONFIG_PATH" -Scope global' in content - - if present: - assert "if ('C:\\tcl' -ne \"\")" in content - assert "$env:TCL_LIBRARY = 'C:\\tcl'" in content - assert "if ('C:\\tk' -ne \"\")" in content - assert "$env:TK_LIBRARY = 'C:\\tk'" in content - assert "if (Test-Path variable:_OLD_VIRTUAL_TCL_LIBRARY)" in content - assert "if (Test-Path variable:_OLD_VIRTUAL_TK_LIBRARY)" in content - else: - assert "if ('' -ne \"\")" in content - assert "$env:TCL_LIBRARY = ''" in content - - -@pytest.mark.slow -def test_powershell(activation_tester_class, activation_tester, monkeypatch) -> None: - monkeypatch.setenv("TERM", "xterm") - - class PowerShell(activation_tester_class): - def __init__(self, session) -> None: - cmd = "powershell.exe" if sys.platform == "win32" else "pwsh" - super().__init__(PowerShellActivator, session, cmd, "activate.ps1", "ps1") - self._version_cmd = [cmd, "-c", "$PSVersionTable"] - self._invoke_script = [cmd, "-NonInteractive", "-NoProfile", "-ExecutionPolicy", "ByPass", "-File"] - self.activate_cmd = "." - self.script_encoding = "utf-8-sig" - - def _get_test_lines(self, activate_script): - return super()._get_test_lines(activate_script) - - def invoke_script(self): - return [self.cmd, "-File"] - - def print_os_env_var(self, var) -> str: - return f'if ($env:{var} -eq $null) {{ "None" }} else {{ $env:{var} }}' - - def print_prompt(self) -> str: - return "prompt" - - def quote(self, s): - """Tester will pass strings to native commands on Windows so extra parsing rules are used. Check `PowerShellActivator.quote` for more details.""" - text = PowerShellActivator.quote(s) - return text.replace('"', '""') if sys.platform == "win32" else text - - def activate_call(self, script): - # Commands are called without quotes in PowerShell - cmd = self.activate_cmd - scr = self.quote(str(script)) - return f"{cmd} {scr}".strip() - - activation_tester(PowerShell) diff --git a/tests/unit/activation/test_python_activator.py b/tests/unit/activation/test_python_activator.py deleted file mode 100644 index 04f0acb6c..000000000 --- a/tests/unit/activation/test_python_activator.py +++ /dev/null @@ -1,126 +0,0 @@ -from __future__ import annotations - -import os -import sys -from argparse import Namespace -from ast import literal_eval -from textwrap import dedent - -from virtualenv.activation import PythonActivator -from virtualenv.info import IS_WIN - - -def test_python_activator_generates_pkg_config_path(tmp_path) -> None: - """Test that activate_this.py sets PKG_CONFIG_PATH.""" - - class MockInterpreter: - tcl_lib = None - tk_lib = None - - class MockCreator: - def __init__(self, dest) -> None: - self.dest = dest - self.bin_dir = dest / ("Scripts" if IS_WIN else "bin") - self.bin_dir.mkdir(parents=True) - self.libs = [dest / "Lib" / "site-packages"] - self.env_name = "test-env" - self.interpreter = MockInterpreter() - self.pyenv_cfg = {} - - creator = MockCreator(tmp_path) - options = Namespace(prompt=None) - activator = PythonActivator(options) - - # Generate the activation script - activator.generate(creator) - - # Read the generated script - content = (creator.bin_dir / "activate_this.py").read_text(encoding="utf-8") - - # Verify PKG_CONFIG_PATH is set - assert "PKG_CONFIG_PATH" in content - assert "pkg_config_path" in content - assert 'os.path.join(base, "lib", "pkgconfig")' in content - - -def test_python(raise_on_non_source_class, activation_tester) -> None: - class Python(raise_on_non_source_class): - def __init__(self, session) -> None: - super().__init__( - PythonActivator, - session, - sys.executable, - activate_script="activate_this.py", - extension="py", - non_source_fail_message="You must use import runpy; runpy.run_path(this_file)", - ) - self.unix_line_ending = not IS_WIN - - def env(self, tmp_path): - env = os.environ.copy() - env["PYTHONIOENCODING"] = "utf-8" - for key in ("VIRTUAL_ENV", "PYTHONPATH"): - env.pop(str(key), None) - env["PATH"] = os.pathsep.join([str(tmp_path), str(tmp_path / "other")]) - return env - - @staticmethod - def _get_test_lines(activate_script): - raw = f""" - import os - import sys - import platform - import runpy - - def print_r(value): - print(repr(value)) - - print_r(os.environ.get("VIRTUAL_ENV")) - print_r(os.environ.get("VIRTUAL_ENV_PROMPT")) - print_r(os.environ.get("PATH").split(os.pathsep)) - print_r(sys.path) - - file_at = {str(activate_script)!r} - # CPython 2 requires non-ascii path open to be unicode - runpy.run_path(file_at) - print_r(os.environ.get("VIRTUAL_ENV")) - print_r(os.environ.get("VIRTUAL_ENV_PROMPT")) - print_r(os.environ.get("PATH").split(os.pathsep)) - print_r(sys.path) - - import pydoc_test - print_r(pydoc_test.__file__) - """ - return dedent(raw).splitlines() - - def assert_output(self, out, raw, tmp_path) -> None: # noqa: ARG002 - out = [literal_eval(i) for i in out] - assert out[0] is None # start with VIRTUAL_ENV None - assert out[1] is None # likewise for VIRTUAL_ENV_PROMPT - - prev_path = out[2] - prev_sys_path = out[3] - assert out[4] == str(self._creator.dest) # VIRTUAL_ENV now points to the virtual env folder - - assert out[5] == str(self._creator.env_name) # VIRTUAL_ENV_PROMPT now has the env name - - new_path = out[6] # PATH now starts with bin path of current - assert ([str(self._creator.bin_dir), *prev_path]) == new_path - - # sys path contains the site package at its start - new_sys_path = out[7] - - new_lib_paths = {str(i) for i in self._creator.libs} - assert prev_sys_path == new_sys_path[len(new_lib_paths) :] - assert new_lib_paths == set(new_sys_path[: len(new_lib_paths)]) - - # manage to import from activate site package - dest = self.norm_path(self._creator.purelib / "pydoc_test.py") - found = self.norm_path(out[8]) - assert found.startswith(dest) - - def non_source_activate(self, activate_script): - act = str(activate_script) - return [*self._invoke_script, "-c", f"exec(open({act!r}).read())"] - - activation_tester(Python) diff --git a/tests/unit/config/cli/test_parser.py b/tests/unit/config/cli/test_parser.py deleted file mode 100644 index b470f5fed..000000000 --- a/tests/unit/config/cli/test_parser.py +++ /dev/null @@ -1,57 +0,0 @@ -from __future__ import annotations - -import os -from contextlib import contextmanager - -import pytest - -from virtualenv.config.cli.parser import VirtualEnvConfigParser, VirtualEnvOptions -from virtualenv.config.ini import IniConfig -from virtualenv.run import session_via_cli - - -@pytest.fixture -def gen_parser_no_conf_env(monkeypatch, tmp_path): - keys_to_delete = {key for key in os.environ if key.startswith("VIRTUALENV_")} - for key in keys_to_delete: - monkeypatch.delenv(key) - monkeypatch.setenv(IniConfig.VIRTUALENV_CONFIG_FILE_ENV_VAR, str(tmp_path / "missing")) - - @contextmanager - def _build(): - parser = VirtualEnvConfigParser() - - def _run(*args): - return parser.parse_args(args=args) - - yield parser, _run - parser.enable_help() - - return _build - - -def test_flag(gen_parser_no_conf_env) -> None: - with gen_parser_no_conf_env() as (parser, run): - parser.add_argument("--clear", dest="clear", action="store_true", help="it", default=False) - result = run() - assert result.clear is False - result = run("--clear") - assert result.clear is True - - -def test_reset_app_data_does_not_conflict_clear() -> None: - options = VirtualEnvOptions() - session_via_cli(["--clear", "venv"], options=options) - assert options.clear is True - assert options.reset_app_data is False - - -def test_builtin_discovery_class_preferred(mocker) -> None: - mocker.patch( - "virtualenv.run.plugin.discovery._get_default_discovery", - return_value=["pluginA", "pluginX", "builtin", "Aplugin", "Xplugin"], - ) - - options = VirtualEnvOptions() - session_via_cli(["venv"], options=options) - assert options.discovery == "builtin" diff --git a/tests/unit/config/test___main__.py b/tests/unit/config/test___main__.py deleted file mode 100644 index 992dcc458..000000000 --- a/tests/unit/config/test___main__.py +++ /dev/null @@ -1,124 +0,0 @@ -from __future__ import annotations - -import re -import sys -from subprocess import PIPE, Popen, check_output -from typing import TYPE_CHECKING, NoReturn - -import pytest - -from virtualenv.__main__ import run_with_catch -from virtualenv.util.error import ProcessCallFailedError - -if TYPE_CHECKING: - from pathlib import Path - - -def test_main() -> None: - process = Popen( - [sys.executable, "-m", "virtualenv", "--help"], - universal_newlines=True, - stdout=PIPE, - encoding="utf-8", - ) - out, _ = process.communicate() - assert not process.returncode - assert out - - -@pytest.fixture -def raise_on_session_done(mocker): - def _func(exception) -> None: - from virtualenv.run import session_via_cli # noqa: PLC0415 - - prev_session = session_via_cli - - def _session_via_cli(args, options=None, setup_logging=True, env=None) -> NoReturn: - prev_session(args, options, setup_logging, env) - raise exception - - mocker.patch("virtualenv.run.session_via_cli", side_effect=_session_via_cli) - - return _func - - -def test_fail_no_traceback(raise_on_session_done, tmp_path, capsys) -> None: - raise_on_session_done(ProcessCallFailedError(code=2, out="out\n", err="err\n", cmd=["something"])) - with pytest.raises(SystemExit) as context: - run_with_catch([str(tmp_path)]) - assert context.value.code == 2 - out, err = capsys.readouterr() - assert out == f"subprocess call failed for [{'something'!r}] with code 2\nout\nSystemExit: 2\n" - assert err == "err\n" - - -def test_discovery_fails_no_discovery_plugin(mocker, tmp_path, capsys) -> None: - mocker.patch("virtualenv.run.plugin.discovery.Discovery.entry_points_for", return_value={}) - with pytest.raises(SystemExit) as context: - run_with_catch([str(tmp_path)]) - assert context.value.code == 1 - out, err = capsys.readouterr() - assert "RuntimeError: No discovery plugin found. Try reinstalling virtualenv to fix this issue." in out - assert not err - - -def test_fail_with_traceback(raise_on_session_done, tmp_path, capsys) -> None: - raise_on_session_done(TypeError("something bad")) - - with pytest.raises(TypeError, match="something bad"): - run_with_catch([str(tmp_path), "--with-traceback"]) - out, err = capsys.readouterr() - assert not out - assert not err - - -@pytest.mark.usefixtures("session_app_data") -def test_session_report_full(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: - run_with_catch([str(tmp_path), "--setuptools", "bundle"]) - out, err = capsys.readouterr() - assert not err - lines = out.splitlines() - regexes = [ - r"created virtual environment .* in \d+ms", - r" creator .*", - r" seeder .*", - r" added seed packages: .*pip==.*, setuptools==.*", - r" activators .*", - ] - _match_regexes(lines, regexes) - - -def _match_regexes(lines, regexes) -> None: - for line, regex in zip(lines, regexes): - comp_regex = re.compile(rf"^{regex}$") - assert comp_regex.match(line), line - - -@pytest.mark.usefixtures("session_app_data") -def test_session_report_minimal(tmp_path, capsys) -> None: - run_with_catch([str(tmp_path), "--activators", "", "--without-pip"]) - out, err = capsys.readouterr() - assert not err - lines = out.splitlines() - regexes = [ - r"created virtual environment .* in \d+ms", - r" creator .*", - ] - _match_regexes(lines, regexes) - - -@pytest.mark.usefixtures("session_app_data") -def test_session_report_subprocess(tmp_path) -> None: - # when called via a subprocess the logging framework should flush and POSIX line normalization happen - out = check_output( - [sys.executable, "-m", "virtualenv", str(tmp_path), "--activators", "powershell", "--without-pip"], - text=True, - encoding="utf-8", - ) - lines = out.split("\n") - regexes = [ - r"created virtual environment .* in \d+ms", - r" creator .*", - r" activators .*", - ] - _match_regexes(lines, regexes) diff --git a/tests/unit/config/test_env_var.py b/tests/unit/config/test_env_var.py deleted file mode 100644 index 3c75558ba..000000000 --- a/tests/unit/config/test_env_var.py +++ /dev/null @@ -1,98 +0,0 @@ -from __future__ import annotations - -import os -from pathlib import Path - -import pytest -from python_discovery import PythonInfo - -from virtualenv.config.cli.parser import VirtualEnvOptions -from virtualenv.config.ini import IniConfig -from virtualenv.create.via_global_ref.builtin.cpython.common import is_macos_brew -from virtualenv.run import session_via_cli - - -@pytest.fixture -def _empty_conf(tmp_path, monkeypatch) -> None: - conf = tmp_path / "conf.ini" - monkeypatch.setenv(IniConfig.VIRTUALENV_CONFIG_FILE_ENV_VAR, str(conf)) - conf.write_text("[virtualenv]", encoding="utf-8") - - -@pytest.mark.usefixtures("_empty_conf") -def test_value_ok(monkeypatch) -> None: - monkeypatch.setenv("VIRTUALENV_VERBOSE", "5") - result = session_via_cli(["venv"]) - assert result.verbosity == 5 - - -@pytest.mark.usefixtures("_empty_conf") -def test_value_bad(monkeypatch, caplog) -> None: - monkeypatch.setenv("VIRTUALENV_VERBOSE", "a") - result = session_via_cli(["venv"]) - assert result.verbosity == 2 - assert len(caplog.messages) == 1 - assert "env var VIRTUALENV_VERBOSE failed to convert" in caplog.messages[0] - assert "invalid literal" in caplog.messages[0] - - -def test_python_via_env_var(monkeypatch) -> None: - options = VirtualEnvOptions() - monkeypatch.setenv("VIRTUALENV_PYTHON", "python3") - session_via_cli(["venv"], options=options) - assert options.python == ["python3"] - - -def test_python_multi_value_via_env_var(monkeypatch) -> None: - options = VirtualEnvOptions() - monkeypatch.setenv("VIRTUALENV_PYTHON", "python3,python2") - session_via_cli(["venv"], options=options) - assert options.python == ["python3", "python2"] - - -def test_python_multi_value_newline_via_env_var(monkeypatch) -> None: - options = VirtualEnvOptions() - monkeypatch.setenv("VIRTUALENV_PYTHON", "python3\npython2") - session_via_cli(["venv"], options=options) - assert options.python == ["python3", "python2"] - - -def test_python_multi_value_prefer_newline_via_env_var(monkeypatch) -> None: - options = VirtualEnvOptions() - monkeypatch.setenv("VIRTUALENV_PYTHON", "python3\npython2,python27") - session_via_cli(["venv"], options=options) - assert options.python == ["python3", "python2,python27"] - - -def test_extra_search_dir_via_env_var(tmp_path, monkeypatch) -> None: - monkeypatch.chdir(tmp_path) - value = f"a{os.linesep}0{os.linesep}b{os.pathsep}c" - monkeypatch.setenv("VIRTUALENV_EXTRA_SEARCH_DIR", str(value)) - (tmp_path / "a").mkdir() - (tmp_path / "b").mkdir() - (tmp_path / "c").mkdir() - result = session_via_cli(["venv"]) - assert result.seeder.extra_search_dir == [Path("a").resolve(), Path("b").resolve(), Path("c").resolve()] - - -@pytest.mark.usefixtures("_empty_conf") -@pytest.mark.skipif(is_macos_brew(PythonInfo.current_system()), reason="no copy on brew") -def test_value_alias(monkeypatch, mocker) -> None: - from virtualenv.config.cli.parser import VirtualEnvConfigParser # noqa: PLC0415 - - prev = VirtualEnvConfigParser._fix_default # noqa: SLF001 - - def func(self, action): - if action.dest == "symlinks": - action.default = True # force symlink to be true - elif action.dest == "copies": - action.default = False # force default copy to be False, we expect env-var to flip it - return prev(self, action) - - mocker.patch("virtualenv.run.VirtualEnvConfigParser._fix_default", side_effect=func, autospec=True) - - monkeypatch.delenv("SYMLINKS", raising=False) - monkeypatch.delenv("VIRTUALENV_COPIES", raising=False) - monkeypatch.setenv("VIRTUALENV_ALWAYS_COPY", "1") - result = session_via_cli(["venv"]) - assert result.creator.symlinks is False diff --git a/tests/unit/config/test_ini.py b/tests/unit/config/test_ini.py deleted file mode 100644 index 80d020642..000000000 --- a/tests/unit/config/test_ini.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import annotations - -import sys -from textwrap import dedent - -import pytest - -from virtualenv.info import IS_PYPY, IS_WIN, fs_supports_symlink -from virtualenv.run import session_via_cli - - -@pytest.mark.skipif(not fs_supports_symlink(), reason="symlink is not supported") -@pytest.mark.xfail( - # https://doc.pypy.org/en/latest/install.html?highlight=symlink#download-a-pre-built-pypy - IS_PYPY and IS_WIN and sys.version_info[0:2] >= (3, 9), - reason="symlink is not supported", -) -def test_ini_can_be_overwritten_by_flag(tmp_path, monkeypatch) -> None: - custom_ini = tmp_path / "conf.ini" - custom_ini.write_text( - dedent( - """ - [virtualenv] - copies = True - """, - ), - encoding="utf-8", - ) - monkeypatch.setenv("VIRTUALENV_CONFIG_FILE", str(custom_ini)) - - result = session_via_cli(["venv", "--symlinks"]) - - symlinks = result.creator.symlinks - assert symlinks is True diff --git a/tests/unit/create/conftest.py b/tests/unit/create/conftest.py deleted file mode 100644 index 1da339701..000000000 --- a/tests/unit/create/conftest.py +++ /dev/null @@ -1,50 +0,0 @@ -"""It's possible to use multiple types of host pythons to create virtual environments and all should work: - -- host installation -- invoking from a venv (if Python 3.3+) -- invoking from an old style virtualenv (<17.0.0) -- invoking from our own venv - -""" - -from __future__ import annotations - -import sys -from subprocess import Popen - -import pytest -from python_discovery import PythonInfo - -CURRENT = PythonInfo.current_system() - - -def root(tmp_path_factory, session_app_data): # noqa: ARG001 - return CURRENT.system_executable - - -def venv(tmp_path_factory, session_app_data): - if CURRENT.is_venv: - return sys.executable - root_python = root(tmp_path_factory, session_app_data) - dest = tmp_path_factory.mktemp("venv") - process = Popen([str(root_python), "-m", "venv", "--without-pip", str(dest)]) - process.communicate() - # sadly creating a virtual environment does not tell us where the executable lives in general case - # so discover using some heuristic - return CURRENT.discover_exe(prefix=str(dest)).original_executable - - -PYTHON = { - "root": root, - "venv": venv, -} - - -@pytest.fixture(params=list(PYTHON.values()), ids=list(PYTHON.keys()), scope="session") -def python(request, tmp_path_factory, session_app_data): - result = request.param(tmp_path_factory, session_app_data) - if isinstance(result, Exception): - pytest.skip(f"could not resolve interpreter based on {request.param.__name__} because {result}") - if result is None: - pytest.skip(f"requires interpreter with {request.param.__name__}") - return result diff --git a/tests/unit/create/console_app/demo/__init__.py b/tests/unit/create/console_app/demo/__init__.py deleted file mode 100644 index 8e396504b..000000000 --- a/tests/unit/create/console_app/demo/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import annotations - - -def run() -> None: - print("magic") # noqa: T201 - - -if __name__ == "__main__": - run() diff --git a/tests/unit/create/console_app/demo/__main__.py b/tests/unit/create/console_app/demo/__main__.py deleted file mode 100644 index 8e396504b..000000000 --- a/tests/unit/create/console_app/demo/__main__.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import annotations - - -def run() -> None: - print("magic") # noqa: T201 - - -if __name__ == "__main__": - run() diff --git a/tests/unit/create/console_app/setup.cfg b/tests/unit/create/console_app/setup.cfg deleted file mode 100644 index eda12f230..000000000 --- a/tests/unit/create/console_app/setup.cfg +++ /dev/null @@ -1,14 +0,0 @@ -[metadata] -name = demo -version = 1.0.0 -description = magic package - -[options] -packages = find: - -[options.entry_points] -console_scripts = - magic=demo.__main__:run - -[bdist_wheel] -universal = true diff --git a/tests/unit/create/console_app/setup.py b/tests/unit/create/console_app/setup.py deleted file mode 100644 index a03590f54..000000000 --- a/tests/unit/create/console_app/setup.py +++ /dev/null @@ -1,5 +0,0 @@ -from __future__ import annotations - -from setuptools import setup - -setup() diff --git a/tests/unit/create/test_creator.py b/tests/unit/create/test_creator.py deleted file mode 100644 index 3a8ad25dc..000000000 --- a/tests/unit/create/test_creator.py +++ /dev/null @@ -1,816 +0,0 @@ -from __future__ import annotations - -import ast -import difflib -import gc -import json -import logging -import os -import shutil -import site -import stat -import subprocess -import sys -import textwrap -import zipfile -from collections import OrderedDict -from itertools import product -from pathlib import Path -from stat import S_IREAD, S_IRGRP, S_IROTH -from textwrap import dedent -from threading import Thread - -import pytest -from python_discovery import PythonInfo - -from virtualenv.__main__ import run, run_with_catch -from virtualenv.create.creator import DEBUG_SCRIPT, Creator, get_env_debug_info -from virtualenv.create.pyenv_cfg import PyEnvCfg -from virtualenv.create.via_global_ref import api -from virtualenv.create.via_global_ref.builtin.cpython.common import is_mac_os_framework, is_macos_brew -from virtualenv.create.via_global_ref.builtin.cpython.cpython3 import CPython3Posix -from virtualenv.info import IS_PYPY, IS_WIN, fs_is_case_sensitive -from virtualenv.run import cli_run, session_via_cli -from virtualenv.run.plugin.creators import CreatorSelector - -CURRENT = PythonInfo.current_system() - - -def test_os_path_sep_not_allowed(tmp_path, capsys) -> None: - target = str(tmp_path / f"a{os.pathsep}b") - err = _non_success_exit_code(capsys, target) - msg = ( - f"destination {target!r} must not contain the path separator ({os.pathsep})" - f" as this would break the activation scripts" - ) - assert msg in err, err - - -def _non_success_exit_code(capsys, target): - with pytest.raises(SystemExit) as context: - run_with_catch(args=[target]) - assert context.value.code != 0 - out, err = capsys.readouterr() - assert "SystemExit: " in out - return err - - -def test_destination_exists_file(tmp_path, capsys) -> None: - target = tmp_path / "out" - target.write_text("", encoding="utf-8") - err = _non_success_exit_code(capsys, str(target)) - msg = f"the destination {target!s} already exists and is a file" - assert msg in err, err - - -@pytest.mark.skipif(sys.platform == "win32", reason="Windows only applies R/O to files") -def test_destination_not_write_able(tmp_path, capsys) -> None: - if hasattr(os, "geteuid") and os.geteuid() == 0: - pytest.skip("no way to check permission restriction when running under root") - - target = tmp_path - prev_mod = target.stat().st_mode - target.chmod(S_IREAD | S_IRGRP | S_IROTH) - try: - err = _non_success_exit_code(capsys, str(target)) - msg = f"the destination . is not write-able at {target!s}" - assert msg in err, err - finally: - target.chmod(prev_mod) - - -def cleanup_sys_path(paths): - from virtualenv.create.creator import HERE # noqa: PLC0415 - - paths = [p.resolve() for p in (Path(os.path.abspath(i)) for i in paths) if p.exists()] - to_remove = [Path(HERE)] - if os.environ.get("PYCHARM_HELPERS_DIR"): - to_remove.extend((Path(os.environ["PYCHARM_HELPERS_DIR"]).parent, Path(os.path.expanduser("~")) / ".PyCharm")) - return [i for i in paths if not any(str(i).startswith(str(t)) for t in to_remove)] - - -@pytest.fixture(scope="session") -def system(session_app_data): - return get_env_debug_info(Path(CURRENT.system_executable), DEBUG_SCRIPT, session_app_data, os.environ) - - -CURRENT_CREATORS = [i for i in CreatorSelector.for_interpreter(CURRENT).key_to_class if i != "builtin"] -CREATE_METHODS = [] -for k, v in CreatorSelector.for_interpreter(CURRENT).key_to_meta.items(): - if k in CURRENT_CREATORS: - if v.can_copy: - if k == "venv" and CURRENT.implementation == "PyPy" and CURRENT.pypy_version_info >= [7, 3, 13]: - continue # https://foss.heptapod.net/pypy/pypy/-/issues/4019 - CREATE_METHODS.append((k, "copies")) - if v.can_symlink: - CREATE_METHODS.append((k, "symlinks")) - - -@pytest.mark.graalpy -@pytest.mark.parametrize( - ("creator", "isolated"), - [pytest.param(*i, id=f"{'-'.join(i[0])}-{i[1]}") for i in product(CREATE_METHODS, ["isolated", "global"])], -) -def test_create_no_seed( # noqa: C901, PLR0912, PLR0913, PLR0915 - python, - creator, - isolated, - system, - coverage_env, - special_name_dir, -) -> None: - dest = special_name_dir - creator_key, method = creator - cmd = [ - "-v", - "-v", - "-p", - str(python), - str(dest), - "--without-pip", - "--activators", - "", - "--creator", - creator_key, - f"--{method}", - ] - if isolated == "global": - cmd.append("--system-site-packages") - result = cli_run(cmd) - creator = result.creator - coverage_env() - if IS_PYPY: - # pypy cleans up file descriptors periodically so our (many) subprocess calls impact file descriptor limits - # force a close of these on system where the limit is low-ish (e.g. MacOS 256) - gc.collect() - purelib = creator.purelib - patch_files = {purelib / f"{'_virtualenv'}.{i}" for i in ("py", "pyc", "pth")} - patch_files.add(purelib / "__pycache__") - content = set(creator.purelib.iterdir()) - patch_files - assert not content, "\n".join(str(i) for i in content) - assert creator.env_name == str(dest.name) - debug = creator.debug - assert "exception" not in debug, f"{debug.get('exception')}\n{debug.get('out')}\n{debug.get('err')}" - sys_path = cleanup_sys_path(debug["sys"]["path"]) - system_sys_path = cleanup_sys_path(system["sys"]["path"]) - our_paths = set(sys_path) - set(system_sys_path) - our_paths_repr = "\n".join(repr(i) for i in our_paths) - - # ensure we have at least one extra path added - assert len(our_paths) >= 1, our_paths_repr - # ensure all additional paths are related to the virtual environment - for path in our_paths: - msg = "\n".join(str(p) for p in system_sys_path) - msg = f"\n{path!s}\ndoes not start with {dest!s}\nhas:\n{msg}" - assert str(path).startswith(str(dest)), msg - # ensure there's at least a site-packages folder as part of the virtual environment added - assert any(p for p in our_paths if p.parts[-1] == "site-packages"), our_paths_repr - - # ensure the global site package is added or not, depending on flag - global_sys_path = system_sys_path[-1] - if isolated == "isolated": - msg = "\n".join(str(j) for j in sys_path) - msg = f"global sys path {global_sys_path!s} is in virtual environment sys path:\n{msg}" - assert global_sys_path not in sys_path, msg - else: - common = [] - for left, right in zip(reversed(system_sys_path), reversed(sys_path)): - if left == right: - common.append(left) - else: - break - - def list_to_str(iterable): - return [str(i) for i in iterable] - - assert common, "\n".join(difflib.unified_diff(list_to_str(sys_path), list_to_str(system_sys_path))) - - # test that the python executables in the bin directory are either: - # - files - # - absolute symlinks outside of the venv - # - relative symlinks inside of the venv - if sys.platform == "win32": - exes = ("python.exe",) - else: - exes = ("python", f"python{sys.version_info.major}", f"python{sys.version_info.major}.{sys.version_info.minor}") - if creator_key == "venv": - # for venv some repackaging does not includes the pythonx.y - exes = exes[:-1] - for exe in exes: - exe_path = creator.bin_dir / exe - assert exe_path.exists(), "\n".join(str(i) for i in creator.bin_dir.iterdir()) - if not exe_path.is_symlink(): # option 1: a real file - continue # it was a file - link = os.readlink(str(exe_path)) - if not os.path.isabs(link): # option 2: a relative symlink - continue - # option 3: an absolute symlink, should point outside the venv - assert not link.startswith(str(creator.dest)) - - if IS_WIN and CURRENT.implementation == "CPython": - python_w = creator.exe.parent / "pythonw.exe" - assert python_w.exists() - assert python_w.read_bytes() != creator.exe.read_bytes() - - if CPython3Posix.pyvenv_launch_patch_active(PythonInfo.from_exe(python)) and creator_key != "venv": - result = subprocess.check_output( - [str(creator.exe), "-c", 'import os; print(os.environ.get("__PYVENV_LAUNCHER__"))'], - text=True, - ).strip() - assert result == "None" - - git_ignore = (dest / ".gitignore").read_text(encoding="utf-8") - assert git_ignore.splitlines() == ["# created by virtualenv automatically", "*"] - - -def test_create_cachedir_tag(tmp_path) -> None: - cachedir_tag_file = tmp_path / "CACHEDIR.TAG" - cli_run([str(tmp_path), "--without-pip", "--activators", ""]) - - expected = """ - Signature: 8a477f597d28d172789f06886806bc55 - # This file is a cache directory tag created by Python virtualenv. - # For information about cache directory tags, see: - # https://bford.info/cachedir/ - """ - assert cachedir_tag_file.read_text(encoding="utf-8") == textwrap.dedent(expected).strip() - - -def test_create_cachedir_tag_exists(tmp_path: Path) -> None: - cachedir_tag_file = tmp_path / "CACHEDIR.TAG" - cachedir_tag_file.write_text("magic", encoding="utf-8") - cli_run([str(tmp_path), "--without-pip", "--activators", ""]) - assert cachedir_tag_file.read_text(encoding="utf-8") == "magic" - - -def test_create_cachedir_tag_exists_override(tmp_path: Path) -> None: - cachedir_tag_file = tmp_path / "CACHEDIR.TAG" - cachedir_tag_file.write_text("magic", encoding="utf-8") - cli_run([str(tmp_path), "--without-pip", "--activators", ""]) - assert cachedir_tag_file.read_text(encoding="utf-8") == "magic" - - -def test_create_vcs_ignore_exists(tmp_path) -> None: - git_ignore = tmp_path / ".gitignore" - git_ignore.write_text("magic", encoding="utf-8") - cli_run([str(tmp_path), "--without-pip", "--activators", ""]) - assert git_ignore.read_text(encoding="utf-8") == "magic" - - -def test_create_vcs_ignore_override(tmp_path) -> None: - git_ignore = tmp_path / ".gitignore" - cli_run([str(tmp_path), "--without-pip", "--no-vcs-ignore", "--activators", ""]) - assert not git_ignore.exists() - - -def test_create_vcs_ignore_exists_override(tmp_path) -> None: - git_ignore = tmp_path / ".gitignore" - git_ignore.write_text("magic", encoding="utf-8") - cli_run([str(tmp_path), "--without-pip", "--no-vcs-ignore", "--activators", ""]) - assert git_ignore.read_text(encoding="utf-8") == "magic" - - -@pytest.mark.skipif(not CURRENT.has_venv, reason="requires interpreter with venv") -def test_venv_fails_not_inline(tmp_path, capsys, mocker) -> None: - if hasattr(os, "geteuid") and os.geteuid() == 0: - pytest.skip("no way to check permission restriction when running under root") - - def _session_via_cli(args, options=None, setup_logging=True, env=None): - session = session_via_cli(args, options, setup_logging, env) - assert session.creator.can_be_inline is False - return session - - mocker.patch("virtualenv.run.session_via_cli", side_effect=_session_via_cli) - before = tmp_path.stat().st_mode - cfg_path = tmp_path / "pyvenv.cfg" - cfg_path.write_text("", encoding="utf-8") - cfg = str(cfg_path) - try: - os.chmod(cfg, stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH) - cmd = ["-p", str(CURRENT.executable), str(tmp_path), "--without-pip", "--creator", "venv"] - with pytest.raises(SystemExit) as context: - run(cmd) - assert context.value.code != 0 - finally: - os.chmod(cfg, before) - out, err = capsys.readouterr() - assert "subprocess call failed for" in out, out - assert "Error:" in err, err - - -@pytest.mark.parametrize("creator", CURRENT_CREATORS) -@pytest.mark.parametrize("clear", [True, False], ids=["clear", "no_clear"]) -def test_create_clear_resets(tmp_path, creator, clear, caplog) -> None: - caplog.set_level(logging.DEBUG) - if creator == "venv" and clear is False: - pytest.skip("venv without clear might fail") - marker = tmp_path / "magic" - cmd = [str(tmp_path), "--seeder", "app-data", "--without-pip", "--creator", creator, "-vvv"] - cli_run(cmd) - - marker.write_text("", encoding="utf-8") # if we a marker file this should be gone on a clear run, remain otherwise - assert marker.exists() - - cli_run(cmd + (["--clear"] if clear else [])) - assert marker.exists() is not clear - - -@pytest.mark.parametrize("creator", CURRENT_CREATORS) -@pytest.mark.parametrize("prompt", [None, "magic", "."]) -def test_prompt_set(tmp_path: Path, creator: str, prompt: str | None, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.chdir(tmp_path) - cmd = [str(tmp_path / "env"), "--seeder", "app-data", "--without-pip", "--creator", creator] - if prompt is not None: - cmd.extend(["--prompt", prompt]) - - result = cli_run(cmd) - cfg = PyEnvCfg.from_file(result.creator.pyenv_cfg.path) - if prompt is None: - assert "prompt" not in cfg - elif creator != "venv": - expected = tmp_path.name if prompt == "." else prompt - assert "prompt" in cfg, list(cfg.content.keys()) - assert cfg["prompt"] == expected - - -@pytest.mark.parametrize("creator", CURRENT_CREATORS) -def test_version_key_in_pyenv_cfg(tmp_path: Path, creator: str) -> None: - result = cli_run([str(tmp_path), "--seeder", "app-data", "--without-pip", "--creator", creator]) - cfg = PyEnvCfg.from_file(result.creator.pyenv_cfg.path) - assert "version" in cfg - parts = cfg["version"].split(".") - assert len(parts) == 3 - assert all(p.isdigit() for p in parts) - - -@pytest.mark.parametrize("creator", [c for c in CURRENT_CREATORS if c != "venv"]) -def test_executable_and_command_keys(tmp_path: Path, creator: str) -> None: - result = cli_run([str(tmp_path), "--seeder", "app-data", "--without-pip", "--creator", creator]) - cfg = PyEnvCfg.from_file(result.creator.pyenv_cfg.path) - assert "executable" in cfg - assert Path(cfg["executable"]).exists() - assert "command" in cfg - assert "virtualenv" in cfg["command"] - - -@pytest.mark.parametrize("creator", [c for c in CURRENT_CREATORS if c != "venv"]) -def test_include_dir_created(tmp_path: Path, creator: str) -> None: - result = cli_run([str(tmp_path), "--seeder", "app-data", "--without-pip", "--creator", creator]) - if sys.platform == "win32": - include = result.creator.dest / "Include" - else: - include = result.creator.dest / "include" - assert include.is_dir() - - -@pytest.mark.parametrize("creator", CURRENT_CREATORS) -def test_home_path_is_exe_parent(tmp_path, creator) -> None: - cmd = [str(tmp_path), "--seeder", "app-data", "--without-pip", "--creator", creator] - - result = cli_run(cmd) - cfg = PyEnvCfg.from_file(result.creator.pyenv_cfg.path) - - # Cannot assume "home" path is a specific value as path resolution may change - # between versions (symlinks, framework paths, etc) but we can check that a - # python executable is present from the configured path per PEP 405 - if sys.platform == "win32": - exes = ("python.exe",) - else: - exes = ( - "python", - f"python{sys.version_info.major}", - f"python{sys.version_info.major}.{sys.version_info.minor}", - ) - - assert any(os.path.exists(os.path.join(cfg["home"], exe)) for exe in exes) - - -@pytest.mark.usefixtures("temp_app_data") -def test_create_parallel(tmp_path) -> None: - def create(count) -> None: - subprocess.check_call( - [sys.executable, "-m", "virtualenv", "-vvv", str(tmp_path / f"venv{count}"), "--without-pip"], - ) - - threads = [Thread(target=create, args=(i,)) for i in range(1, 4)] - for thread in threads: - thread.start() - for thread in threads: - thread.join() - - -def test_creator_input_passed_is_abs(tmp_path, monkeypatch) -> None: - monkeypatch.chdir(tmp_path) - result = Creator.validate_dest("venv") - assert str(result) == str(tmp_path / "venv") - - -@pytest.mark.skipif(os.altsep is None, reason="OS does not have an altsep") -def test_creator_replaces_altsep_in_dest(tmp_path) -> None: - dest = str(tmp_path / "venv{}foobar") - result = Creator.validate_dest(dest.format(os.altsep)) - assert str(result) == dest.format(os.sep) - - -@pytest.mark.usefixtures("current_fastest") -def test_create_long_path(tmp_path) -> None: - if sys.platform == "darwin": - max_shebang_length = 512 - else: - max_shebang_length = 127 - # filenames can be at most 255 long on macOS, so split to to levels - count = max_shebang_length - len(str(tmp_path)) - folder = tmp_path / ("a" * (count // 2)) / ("b" * (count // 2)) / "c" - folder.mkdir(parents=True) - - cmd = [str(folder), "--without-pip"] - result = cli_run(cmd) - subprocess.check_call([str(result.creator.exe), "--version"]) - - -@pytest.mark.slow -@pytest.mark.parametrize( - "creator", sorted(set(CreatorSelector.for_interpreter(PythonInfo.current_system()).key_to_class) - {"builtin"}) -) -@pytest.mark.usefixtures("session_app_data") -def test_create_distutils_cfg(creator, tmp_path, monkeypatch) -> None: - result = cli_run( - [ - str(tmp_path / "venv"), - "--activators", - "", - "--creator", - creator, - "--setuptools", - "bundle", - ], - ) - - app = Path(__file__).parent / "console_app" - dest = tmp_path / "console_app" - shutil.copytree(str(app), str(dest)) - - setup_cfg = dest / "setup.cfg" - conf = dedent( - f""" - [install] - prefix={tmp_path}{os.sep}prefix - install_purelib={tmp_path}{os.sep}purelib - install_platlib={tmp_path}{os.sep}platlib - install_headers={tmp_path}{os.sep}headers - install_scripts={tmp_path}{os.sep}scripts - install_data={tmp_path}{os.sep}data - """, - ) - setup_cfg.write_text(setup_cfg.read_text(encoding="utf-8") + conf, encoding="utf-8") - - monkeypatch.chdir(dest) # distutils will read the setup.cfg from the cwd, so change to that - - install_demo_cmd = [ - str(result.creator.script("pip")), - "--disable-pip-version-check", - "install", - str(dest), - "-vv", - ] - subprocess.check_call(install_demo_cmd) - - magic = result.creator.script("magic") # console scripts are created in the right location - assert magic.exists() - - package_folder = result.creator.purelib / "demo" # prefix is set to the virtualenv prefix for install - assert package_folder.exists(), list_files(str(tmp_path)) - - -def list_files(path): - result = "" - for root, _, files in os.walk(path): - level = root.replace(path, "").count(os.sep) - indent = " " * 4 * level - result += f"{indent}{os.path.basename(root)}/\n" - sub = " " * 4 * (level + 1) - for f in files: - result += f"{sub}{f}\n" - return result - - -@pytest.mark.skipif(is_macos_brew(CURRENT), reason="no copy on brew") -@pytest.mark.skip(reason="https://github.com/pypa/setuptools/issues/4640") -def test_zip_importer_can_import_setuptools(tmp_path) -> None: - """We're patching the loaders so might fail on r/o loaders, such as zipimporter on CPython<3.8""" - result = cli_run( - [str(tmp_path / "venv"), "--activators", "", "--no-pip", "--no-wheel", "--copies", "--setuptools", "bundle"], - ) - zip_path = tmp_path / "site-packages.zip" - with zipfile.ZipFile(str(zip_path), "w", zipfile.ZIP_DEFLATED) as zip_handler: - lib = str(result.creator.purelib) - for root, _, files in os.walk(lib): - base = root[len(lib) :].lstrip(os.pathsep) - for file in files: - if not file.startswith("_virtualenv"): - zip_handler.write(filename=os.path.join(root, file), arcname=os.path.join(base, file)) - for folder in result.creator.purelib.iterdir(): - if not folder.name.startswith("_virtualenv"): - if folder.is_dir(): - shutil.rmtree(str(folder), ignore_errors=True) - else: - folder.unlink() - env = os.environ.copy() - env["PYTHONPATH"] = str(zip_path) - subprocess.check_call([str(result.creator.exe), "-c", "from setuptools.dist import Distribution"], env=env) - - -# verify that python in created virtualenv does not preimport threading. -# https://github.com/pypa/virtualenv/issues/1895 -# -# coverage is disabled, because when coverage is active, it imports threading in default mode. -@pytest.mark.xfail( - IS_PYPY and sys.platform.startswith("darwin"), - reason="https://foss.heptapod.net/pypy/pypy/-/issues/3269", -) -@pytest.mark.usefixtures("_no_coverage") -def test_no_preimport_threading(tmp_path) -> None: - session = cli_run([str(tmp_path)]) - out = subprocess.check_output( - [str(session.creator.exe), "-c", r"import sys; print('\n'.join(sorted(sys.modules)))"], - text=True, - encoding="utf-8", - ) - imported = set(out.splitlines()) - assert "threading" not in imported - - -# verify that .pth files in site-packages/ are always processed even if $PYTHONPATH points to it. -def test_pth_in_site_vs_python_path(tmp_path) -> None: - session = cli_run([str(tmp_path)]) - site_packages = session.creator.purelib - # install test.pth that sets sys.testpth='ok' - (session.creator.purelib / "test.pth").write_text('import sys; sys.testpth="ok"\n', encoding="utf-8") - # verify that test.pth is activated when interpreter is run - out = subprocess.check_output( - [str(session.creator.exe), "-c", r"import sys; print(sys.testpth)"], - text=True, - encoding="utf-8", - ) - assert out == "ok\n" - # same with $PYTHONPATH pointing to site_packages - env = os.environ.copy() - path = [str(site_packages)] - if "PYTHONPATH" in env: - path.append(env["PYTHONPATH"]) - env["PYTHONPATH"] = os.pathsep.join(path) - out = subprocess.check_output( - [str(session.creator.exe), "-c", r"import sys; print(sys.testpth)"], - text=True, - env=env, - encoding="utf-8", - ) - assert out == "ok\n" - - -def test_getsitepackages_system_site(tmp_path) -> None: - # Test without --system-site-packages - session = cli_run([str(tmp_path)]) - - system_site_packages = get_expected_system_site_packages(session) - - out = subprocess.check_output( - [str(session.creator.exe), "-c", r"import site; print(site.getsitepackages())"], - text=True, - encoding="utf-8", - ) - site_packages = ast.literal_eval(out) - - for system_site_package in system_site_packages: - assert system_site_package not in site_packages - - # Test with --system-site-packages - session = cli_run([str(tmp_path), "--system-site-packages"]) - - system_site_packages = [str(Path(i).resolve()) for i in get_expected_system_site_packages(session)] - - out = subprocess.check_output( - [str(session.creator.exe), "-c", r"import site; print(site.getsitepackages())"], - text=True, - encoding="utf-8", - ) - site_packages = [str(Path(i).resolve()) for i in ast.literal_eval(out)] - - for system_site_package in system_site_packages: - assert system_site_package in site_packages - - -def get_expected_system_site_packages(session): - base_prefix = session.creator.pyenv_cfg["base-prefix"] - base_exec_prefix = session.creator.pyenv_cfg["base-exec-prefix"] - old_prefixes = site.PREFIXES - site.PREFIXES = [base_prefix, base_exec_prefix] - system_site_packages = site.getsitepackages() - site.PREFIXES = old_prefixes - - return system_site_packages - - -def test_get_site_packages(tmp_path) -> None: - case_sensitive = fs_is_case_sensitive() - session = cli_run([str(tmp_path)]) - env_site_packages = [str(session.creator.purelib), str(session.creator.platlib)] - out = subprocess.check_output( - [str(session.creator.exe), "-c", r"import site; print(site.getsitepackages())"], - text=True, - encoding="utf-8", - ) - site_packages = ast.literal_eval(out) - - if not case_sensitive: - env_site_packages = [x.lower() for x in env_site_packages] - site_packages = [x.lower() for x in site_packages] - - for env_site_package in env_site_packages: - assert env_site_package in site_packages - - -def test_debug_bad_virtualenv(tmp_path) -> None: - cmd = [str(tmp_path), "--without-pip"] - result = cli_run(cmd) - # if the site.py is removed/altered the debug should fail as no one is around to fix the paths - cust = result.creator.purelib / "_a.pth" - cust.write_text( - 'import sys; sys.stdout.write("std-out"); sys.stderr.write("std-err"); raise SystemExit(1)', - encoding="utf-8", - ) - debug_info = result.creator.debug - assert debug_info["returncode"] == 1 - assert "std-err" in debug_info["err"] - assert "std-out" in debug_info["out"] - assert debug_info["exception"] - - -@pytest.mark.graalpy -@pytest.mark.parametrize("python_path_on", [True, False], ids=["on", "off"]) -def test_python_path(monkeypatch, tmp_path, python_path_on) -> None: - result = cli_run([str(tmp_path), "--without-pip", "--activators", ""]) - monkeypatch.chdir(tmp_path) - case_sensitive = fs_is_case_sensitive() - - def _get_sys_path(flag=None): - cmd = [str(result.creator.exe)] - if flag: - cmd.append(flag) - cmd.extend(["-c", "import json; import sys; print(json.dumps(sys.path))"]) - return [i if case_sensitive else i.lower() for i in json.loads(subprocess.check_output(cmd, encoding="utf-8"))] - - monkeypatch.delenv("PYTHONPATH", raising=False) - base = _get_sys_path() - - # note the value result.creator.interpreter.system_stdlib cannot be set, as that would disable our custom site.py - python_paths = [ - str(Path(result.creator.interpreter.prefix)), - str(Path(result.creator.interpreter.system_stdlib) / "b"), - str(result.creator.purelib / "a"), - str(result.creator.purelib), - str(result.creator.bin_dir), - str(tmp_path / "base"), - f"{tmp_path / 'base_sep'!s}{os.sep}", - "name", - f"name{os.sep}", - f"{tmp_path.parent}{f'{tmp_path.name}_suffix'}", - ".", - "..", - "", - ] - python_path_env = os.pathsep.join(python_paths) - monkeypatch.setenv("PYTHONPATH", python_path_env) - - extra_all = _get_sys_path(None if python_path_on else "-E") - if python_path_on: - assert not extra_all[0] # the cwd is always injected at start as '' - extra_all = extra_all[1:] - assert not base[0] - base = base[1:] - - assert not (set(base) - set(extra_all)) # all base paths are present - abs_python_paths = list(OrderedDict((os.path.abspath(str(i)), None) for i in python_paths).keys()) - abs_python_paths = [i if case_sensitive else i.lower() for i in abs_python_paths] - - extra_as_python_path = extra_all[: len(abs_python_paths)] - assert abs_python_paths == extra_as_python_path # python paths are there at the start - - non_python_path = extra_all[len(abs_python_paths) :] - assert non_python_path == [i for i in base if i not in extra_as_python_path] - else: - assert base == extra_all - - -# Make sure that the venv creator works on systems where vendor-delivered files -# (specifically venv scripts delivered with Python itself) are not writable. -# -# https://github.com/pypa/virtualenv/issues/2419 -@pytest.mark.skipif("venv" not in CURRENT_CREATORS, reason="test needs venv creator") -def test_venv_creator_without_write_perms(tmp_path, mocker) -> None: - from virtualenv.run.session import Session # noqa: PLC0415 - - prev = Session._create # noqa: SLF001 - - def func(self) -> None: - prev(self) - scripts_dir = self.creator.dest / "bin" - for script in scripts_dir.glob("*ctivate*"): - script.chmod(stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH) - - mocker.patch("virtualenv.run.session.Session._create", side_effect=func, autospec=True) - - cmd = [str(tmp_path), "--seeder", "app-data", "--without-pip", "--creator", "venv"] - cli_run(cmd) - - -def test_fallback_to_copies_if_symlink_unsupported(tmp_path, python, mocker) -> None: - """Test that creating a virtual environment falls back to copies when filesystem has no symlink support.""" - if is_macos_brew(PythonInfo.from_exe(python)): - pytest.skip("brew python on darwin may not support copies, which is tested separately") - - # Given a filesystem that does not support symlinks - mocker.patch("virtualenv.create.via_global_ref.api.fs_supports_symlink", return_value=False) - - # When creating a virtual environment (no method specified) - cmd = [ - "-v", - "-p", - str(python), - str(tmp_path), - "--without-pip", - "--activators", - "", - ] - result = cli_run(cmd) - - # Then the creation should succeed and the creator should report it used copies - assert result.creator is not None - assert result.creator.symlinks is False - - -def test_fail_gracefully_if_no_method_supported(tmp_path, python, mocker) -> None: - """Test that virtualenv fails gracefully when no creation method is supported.""" - # Given a filesystem that does not support symlinks - mocker.patch("virtualenv.create.via_global_ref.api.fs_supports_symlink", return_value=False) - - # And a creator that does not support copying - if not is_macos_brew(PythonInfo.from_exe(python)): - original_init = api.ViaGlobalRefMeta.__init__ - - def new_init(self, *args, **kwargs) -> None: - original_init(self, *args, **kwargs) - self.copy_error = "copying is not supported" - - mocker.patch("virtualenv.create.via_global_ref.api.ViaGlobalRefMeta.__init__", new=new_init) - - # When creating a virtual environment - with pytest.raises(RuntimeError) as excinfo: - cli_run( - [ - "-p", - str(python), - str(tmp_path), - "--without-pip", - ], - ) - - # Then a RuntimeError should be raised with a detailed message - assert "neither symlink or copy method supported" in str(excinfo.value) - assert "symlink: the filesystem does not supports symlink" in str(excinfo.value) - interpreter = PythonInfo.from_exe(python) - if is_macos_brew(interpreter): - assert "copy: Brew disables copy creation" in str(excinfo.value) - elif is_mac_os_framework(interpreter): - assert "copy: macOS framework builds do not support copy-based virtual environments" in str(excinfo.value) - else: - assert "copy: copying is not supported" in str(excinfo.value) - - -def test_pyenv_cfg_preserves_symlinks(tmp_path) -> None: - """Test that PyEnvCfg.write() preserves symlinks and doesn't resolve them (issue #2770).""" - # Create a real directory and a symlink to it - real_dir = tmp_path / "real_directory" - real_dir.mkdir() - real_file = real_dir / "some_file.txt" - real_file.write_text("test") - - symlink_dir = tmp_path / "symlink_directory" - try: - symlink_dir.symlink_to(real_dir, target_is_directory=True) - except (OSError, NotImplementedError): - pytest.skip("Symlinks not supported on this platform") - - cfg_path = tmp_path / "pyvenv.cfg" - cfg = PyEnvCfg(OrderedDict(), cfg_path) - - symlink_path = str(symlink_dir / "some_file.txt") - cfg["test_path"] = symlink_path - cfg.write() - - written_content = cfg_path.read_text() - expected_abspath = os.path.abspath(symlink_path) - expected_realpath = os.path.realpath(symlink_path) - - assert f"test_path = {expected_abspath}" in written_content - assert expected_abspath != expected_realpath, "Test setup error: paths should differ for symlinks" diff --git a/tests/unit/create/test_interpreters.py b/tests/unit/create/test_interpreters.py deleted file mode 100644 index 73668ce2e..000000000 --- a/tests/unit/create/test_interpreters.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations - -import sys -from uuid import uuid4 - -import pytest -from python_discovery import PythonInfo - -from virtualenv.run import cli_run - - -@pytest.mark.slow -def test_failed_to_find_bad_spec() -> None: - of_id = uuid4().hex - with pytest.raises(RuntimeError) as context: - cli_run(["-p", of_id]) - msg = repr(RuntimeError(f"failed to find interpreter for Builtin discover of python_spec={of_id!r}")) - assert repr(context.value) == msg - - -SYSTEM = PythonInfo.current_system() - - -@pytest.mark.parametrize( - "of_id", - ({sys.executable} if sys.executable != SYSTEM.executable else set()) | {SYSTEM.implementation}, -) -def test_failed_to_find_implementation(of_id, mocker) -> None: - mocker.patch("virtualenv.run.plugin.creators.CreatorSelector._OPTIONS", return_value={}) - with pytest.raises(RuntimeError) as context: - cli_run(["-p", of_id]) - assert repr(context.value) == repr(RuntimeError(f"No virtualenv implementation for {PythonInfo.current_system()}")) diff --git a/tests/unit/create/via_global_ref/_test_race_condition_helper.py b/tests/unit/create/via_global_ref/_test_race_condition_helper.py deleted file mode 100644 index 732f95f0c..000000000 --- a/tests/unit/create/via_global_ref/_test_race_condition_helper.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations - -from typing import ClassVar - - -class _Finder: - fullname = None - lock: ClassVar[list] = [] - - def find_spec(self, fullname, path, target=None) -> None: # noqa: ARG002 - # This should handle the NameError gracefully - try: - distutils_patch = _DISTUTILS_PATCH - except NameError: - return - if fullname in distutils_patch and self.fullname is None: - return - return - - @staticmethod - def exec_module(old, module) -> None: - old(module) - try: - distutils_patch = _DISTUTILS_PATCH - except NameError: - return - if module.__name__ in distutils_patch: - pass # Would call patch_dist(module) - - @staticmethod - def load_module(old, name): - module = old(name) - try: - distutils_patch = _DISTUTILS_PATCH - except NameError: - return module - if module.__name__ in distutils_patch: - pass # Would call patch_dist(module) - return module - - -finder = _Finder() diff --git a/tests/unit/create/via_global_ref/builtin/conftest.py b/tests/unit/create/via_global_ref/builtin/conftest.py deleted file mode 100644 index 7d913fb96..000000000 --- a/tests/unit/create/via_global_ref/builtin/conftest.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path -from typing import TYPE_CHECKING -from unittest.mock import MagicMock - -import pytest -from testing import path -from testing.py_info import read_fixture - -if TYPE_CHECKING: - from tests.types import Interpreter, MakeInterpreter - -# Allows to import from `testing` into test submodules. -sys.path.append(str(Path(__file__).parent)) - - -@pytest.fixture -def py_info(py_info_name): - return read_fixture(py_info_name) - - -@pytest.fixture -def mock_files(mocker): - return lambda paths, files: path.mock_files(mocker, paths, files) - - -@pytest.fixture -def mock_pypy_libs(mocker): - return lambda pypy, libs: path.mock_pypy_libs(mocker, pypy, libs) - - -@pytest.fixture -def make_interpreter() -> MakeInterpreter: - def _make( - sysconfig_vars: dict[str, object] | None = None, - prefix: str = "/usr", - free_threaded: bool = False, - version_info: tuple[int, ...] = (3, 14, 0), - ) -> Interpreter: - interpreter = MagicMock() - interpreter.prefix = prefix - interpreter.system_prefix = prefix - interpreter.system_executable = f"{prefix}/bin/python3" - interpreter.free_threaded = free_threaded - interpreter.version_info = MagicMock(major=version_info[0], minor=version_info[1]) - interpreter.sysconfig_vars = sysconfig_vars or {} - return interpreter - - return _make # type: ignore[return-value] diff --git a/tests/unit/create/via_global_ref/builtin/cpython/conftest.py b/tests/unit/create/via_global_ref/builtin/cpython/conftest.py deleted file mode 100644 index 9a3ce4ad2..000000000 --- a/tests/unit/create/via_global_ref/builtin/cpython/conftest.py +++ /dev/null @@ -1,35 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pytest - -from virtualenv.create.via_global_ref.builtin.cpython.cpython3 import CPython3Posix - -if TYPE_CHECKING: - from collections.abc import Callable - from pathlib import Path - - from pytest_mock import MockerFixture - - from tests.types import MakeInterpreter - from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest - - CollectSources = Callable[[dict[str, object]], list[PathRefToDest]] - - -@pytest.fixture -def collect_sources(tmp_path: Path, make_interpreter: MakeInterpreter, mocker: MockerFixture) -> CollectSources: - def _collect(sysconfig_vars: dict[str, object]) -> list[PathRefToDest]: - interpreter = make_interpreter( - sysconfig_vars={**sysconfig_vars, "PYTHONFRAMEWORK": ""}, - prefix=str(tmp_path), - ) - interpreter.system_executable = str(tmp_path / "bin" / "python3") - mocker.patch( - "virtualenv.create.via_global_ref.builtin.cpython.cpython3.Path.exists", - return_value=True, - ) - return list(CPython3Posix.sources(interpreter)) - - return _collect diff --git a/tests/unit/create/via_global_ref/builtin/cpython/cpython3_win_embed.json b/tests/unit/create/via_global_ref/builtin/cpython/cpython3_win_embed.json deleted file mode 100644 index c75c6f4fc..000000000 --- a/tests/unit/create/via_global_ref/builtin/cpython/cpython3_win_embed.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "platform": "win32", - "implementation": "CPython", - "version_info": { - "major": 3, - "minor": 10, - "micro": 4, - "releaselevel": "final", - "serial": 0 - }, - "architecture": 64, - "version_nodot": "310", - "version": "3.10.4 (tags/v3.10.4:9d38120, Mar 23 2022, 23:13:41) [MSC v.1929 64 bit (AMD64)]", - "os": "nt", - "prefix": "c:\\path\\to\\python", - "base_prefix": "c:\\path\\to\\python", - "real_prefix": null, - "base_exec_prefix": "c:\\path\\to\\python", - "exec_prefix": "c:\\path\\to\\python", - "executable": "c:\\path\\to\\python\\python.exe", - "original_executable": "c:\\path\\to\\python\\python.exe", - "system_executable": "c:\\path\\to\\python\\python.exe", - "has_venv": false, - "path": [ - "c:\\path\\to\\python\\Scripts\\virtualenv.exe", - "c:\\path\\to\\python\\python310.zip", - "c:\\path\\to\\python", - "c:\\path\\to\\python\\Lib\\site-packages" - ], - "file_system_encoding": "utf-8", - "stdout_encoding": "utf-8", - "sysconfig_scheme": null, - "sysconfig_paths": { - "stdlib": "{installed_base}/Lib", - "platstdlib": "{base}/Lib", - "purelib": "{base}/Lib/site-packages", - "platlib": "{base}/Lib/site-packages", - "include": "{installed_base}/Include", - "scripts": "{base}/Scripts", - "data": "{base}" - }, - "distutils_install": { - "purelib": "Lib\\site-packages", - "platlib": "Lib\\site-packages", - "headers": "Include\\UNKNOWN", - "scripts": "Scripts", - "data": "" - }, - "sysconfig": { - "makefile_filename": "c:\\path\\to\\python\\Lib\\config\\Makefile" - }, - "sysconfig_vars": { - "PYTHONFRAMEWORK": "", - "installed_base": "c:\\path\\to\\python", - "base": "c:\\path\\to\\python" - }, - "system_stdlib": "c:\\path\\to\\python\\Lib", - "system_stdlib_platform": "c:\\path\\to\\python\\Lib", - "max_size": 9223372036854775807, - "_creators": null, - "free_threaded": false -} diff --git a/tests/unit/create/via_global_ref/builtin/cpython/cpython3_win_free_threaded.json b/tests/unit/create/via_global_ref/builtin/cpython/cpython3_win_free_threaded.json deleted file mode 100644 index f39a6059c..000000000 --- a/tests/unit/create/via_global_ref/builtin/cpython/cpython3_win_free_threaded.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "platform": "win32", - "implementation": "CPython", - "version_info": { - "major": 3, - "minor": 13, - "micro": 0, - "releaselevel": "final", - "serial": 0 - }, - "architecture": 64, - "version_nodot": "313", - "version": "3.13.0 (main, Oct 07 2024, 00:00:00) [MSC v.1941 64 bit (AMD64)]", - "os": "nt", - "prefix": "c:\\path\\to\\python", - "base_prefix": "c:\\path\\to\\python", - "real_prefix": null, - "base_exec_prefix": "c:\\path\\to\\python", - "exec_prefix": "c:\\path\\to\\python", - "executable": "c:\\path\\to\\python\\python3.13t.exe", - "original_executable": "c:\\path\\to\\python\\python3.13t.exe", - "system_executable": "c:\\path\\to\\python\\python3.13t.exe", - "has_venv": false, - "path": [ - "c:\\path\\to\\python\\Scripts\\virtualenv.exe", - "c:\\path\\to\\python\\python313.zip", - "c:\\path\\to\\python", - "c:\\path\\to\\python\\Lib\\site-packages" - ], - "file_system_encoding": "utf-8", - "stdout_encoding": "utf-8", - "sysconfig_scheme": null, - "sysconfig_paths": { - "stdlib": "{installed_base}/Lib", - "platstdlib": "{base}/Lib", - "purelib": "{base}/Lib/site-packages", - "platlib": "{base}/Lib/site-packages", - "include": "{installed_base}/Include", - "scripts": "{base}/Scripts", - "data": "{base}" - }, - "distutils_install": { - "purelib": "Lib\\site-packages", - "platlib": "Lib\\site-packages", - "headers": "Include\\UNKNOWN", - "scripts": "Scripts", - "data": "" - }, - "sysconfig": { - "makefile_filename": "c:\\path\\to\\python\\Lib\\config\\Makefile" - }, - "sysconfig_vars": { - "PYTHONFRAMEWORK": "", - "installed_base": "c:\\path\\to\\python", - "base": "c:\\path\\to\\python" - }, - "system_stdlib": "c:\\path\\to\\python\\Lib", - "system_stdlib_platform": "c:\\path\\to\\python\\Lib", - "max_size": 9223372036854775807, - "_creators": null, - "free_threaded": true -} diff --git a/tests/unit/create/via_global_ref/builtin/cpython/test_cpython3_posix.py b/tests/unit/create/via_global_ref/builtin/cpython/test_cpython3_posix.py deleted file mode 100644 index 53ced18b2..000000000 --- a/tests/unit/create/via_global_ref/builtin/cpython/test_cpython3_posix.py +++ /dev/null @@ -1,59 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from virtualenv.create.via_global_ref.builtin.cpython.cpython3 import CPython3Posix -from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest, RefWhen - -if TYPE_CHECKING: - from pathlib import Path - - from conftest import CollectSources - - from tests.types import MakeInterpreter - - -def _shared_lib_copy_refs(sources: list[PathRefToDest]) -> list[PathRefToDest]: - return [s for s in sources if isinstance(s, PathRefToDest) and s.when == RefWhen.COPY] - - -def test_shared_lib_included(tmp_path: Path, collect_sources: CollectSources) -> None: - lib_dir = tmp_path / "lib" - lib_dir.mkdir() - (lib_dir / "libpython3.14.so").touch() - sources = collect_sources({"Py_ENABLE_SHARED": 1, "INSTSONAME": "libpython3.14.so", "LIBDIR": str(lib_dir)}) - shared_refs = _shared_lib_copy_refs(sources) - assert len(shared_refs) == 1 - assert shared_refs[0].src.name == "libpython3.14.so" - - -def test_shared_lib_excluded_when_static(collect_sources: CollectSources) -> None: - sources = collect_sources({"Py_ENABLE_SHARED": 0}) - assert _shared_lib_copy_refs(sources) == [] - - -def test_shared_lib_excluded_when_no_lib_name(collect_sources: CollectSources) -> None: - sources = collect_sources({"Py_ENABLE_SHARED": 1}) - assert _shared_lib_copy_refs(sources) == [] - - -def test_shared_lib_excluded_when_no_lib_dir(collect_sources: CollectSources) -> None: - sources = collect_sources({"Py_ENABLE_SHARED": 1, "INSTSONAME": "libpython3.14.so"}) - assert _shared_lib_copy_refs(sources) == [] - - -def test_shared_lib_excluded_when_file_missing(tmp_path: Path, make_interpreter: MakeInterpreter) -> None: - lib_dir = tmp_path / "lib" - lib_dir.mkdir() - interpreter = make_interpreter( - sysconfig_vars={ - "Py_ENABLE_SHARED": 1, - "INSTSONAME": "libpython3.14.so", - "LIBDIR": str(lib_dir), - "PYTHONFRAMEWORK": "", - }, - prefix=str(tmp_path), - ) - interpreter.system_executable = str(tmp_path / "bin" / "python3") - sources = list(CPython3Posix.sources(interpreter)) - assert _shared_lib_copy_refs(sources) == [] diff --git a/tests/unit/create/via_global_ref/builtin/cpython/test_cpython3_win.py b/tests/unit/create/via_global_ref/builtin/cpython/test_cpython3_win.py deleted file mode 100644 index 94e02bfb9..000000000 --- a/tests/unit/create/via_global_ref/builtin/cpython/test_cpython3_win.py +++ /dev/null @@ -1,161 +0,0 @@ -from __future__ import annotations - -import pytest -from testing.helpers import contains_exe, contains_ref, has_src, is_exe -from testing.path import join as path - -from virtualenv.create.via_global_ref.builtin.cpython.cpython3 import CPython3Windows - -CPYTHON3_PATH = ( - "virtualenv.create.via_global_ref.builtin.cpython.common.Path", - "virtualenv.create.via_global_ref.builtin.cpython.cpython3.Path", -) - - -@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"]) -def test_2_exe_on_default_py_host(py_info, mock_files) -> None: - mock_files(CPYTHON3_PATH, [py_info.system_executable]) - sources = tuple(CPython3Windows.sources(interpreter=py_info)) - # Default Python exe. - assert contains_exe(sources, py_info.system_executable) - # Should always exist. - assert contains_exe(sources, path(py_info.prefix, "pythonw.exe")) - - -@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"]) -def test_3_exe_on_not_default_py_host(py_info, mock_files) -> None: - # Not default python host. - py_info.system_executable = path(py_info.prefix, "python666.exe") - mock_files(CPYTHON3_PATH, [py_info.system_executable]) - sources = tuple(CPython3Windows.sources(interpreter=py_info)) - # Not default Python exe linked to both the default name and origin. - assert contains_exe(sources, py_info.system_executable, "python.exe") - assert contains_exe(sources, py_info.system_executable, "python666.exe") - # Should always exist. - assert contains_exe(sources, path(py_info.prefix, "pythonw.exe")) - - -@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"]) -def test_only_shim(py_info, mock_files) -> None: - shim = path(py_info.system_stdlib, "venv\\scripts\\nt\\python.exe") - py_files = ( - path(py_info.prefix, "libcrypto-1_1.dll"), - path(py_info.prefix, "libffi-7.dll"), - path(py_info.prefix, "_asyncio.pyd"), - path(py_info.prefix, "_bz2.pyd"), - ) - mock_files(CPYTHON3_PATH, [shim, *py_files]) - sources = tuple(CPython3Windows.sources(interpreter=py_info)) - assert CPython3Windows.has_shim(interpreter=py_info) - assert contains_exe(sources, shim) - assert not contains_exe(sources, py_info.system_executable) - for file in py_files: - assert not contains_ref(sources, file) - - -@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"]) -def test_exe_dll_pyd_without_shim(py_info, mock_files) -> None: - py_files = ( - path(py_info.prefix, "libcrypto-1_1.dll"), - path(py_info.prefix, "libffi-7.dll"), - path(py_info.prefix, "_asyncio.pyd"), - path(py_info.prefix, "_bz2.pyd"), - ) - mock_files(CPYTHON3_PATH, py_files) - sources = tuple(CPython3Windows.sources(interpreter=py_info)) - assert not CPython3Windows.has_shim(interpreter=py_info) - assert contains_exe(sources, py_info.system_executable) - for file in py_files: - assert contains_ref(sources, file) - - -@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"]) -def test_python_zip_if_exists_and_set_in_path(py_info, mock_files) -> None: - python_zip_name = f"python{py_info.version_nodot}.zip" - python_zip = path(py_info.prefix, python_zip_name) - mock_files(CPYTHON3_PATH, [python_zip]) - sources = tuple(CPython3Windows.sources(interpreter=py_info)) - assert python_zip in py_info.path - assert contains_ref(sources, python_zip) - - -@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"]) -def test_no_python_zip_if_exists_and_not_set_in_path(py_info, mock_files) -> None: - python_zip_name = f"python{py_info.version_nodot}.zip" - python_zip = path(py_info.prefix, python_zip_name) - py_info.path.remove(python_zip) - mock_files(CPYTHON3_PATH, [python_zip]) - sources = tuple(CPython3Windows.sources(interpreter=py_info)) - assert python_zip not in py_info.path - assert not contains_ref(sources, python_zip) - - -@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"]) -def test_no_python_zip_if_not_exists(py_info, mock_files) -> None: - python_zip_name = f"python{py_info.version_nodot}.zip" - python_zip = path(py_info.prefix, python_zip_name) - # No `python_zip`, just python.exe file. - mock_files(CPYTHON3_PATH, [py_info.system_executable]) - sources = tuple(CPython3Windows.sources(interpreter=py_info)) - assert python_zip in py_info.path - assert not contains_ref(sources, python_zip) - - -@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"]) -def test_python3_exe_present(py_info, mock_files) -> None: - mock_files(CPYTHON3_PATH, [py_info.system_executable]) - sources = tuple(CPython3Windows.sources(interpreter=py_info)) - assert contains_exe(sources, py_info.system_executable, "python3.exe") - assert contains_exe(sources, py_info.system_executable, "python3") - - -@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"]) -def test_pythonw3_exe_present(py_info, mock_files) -> None: - mock_files(CPYTHON3_PATH, [py_info.system_executable]) - sources = tuple(CPython3Windows.sources(interpreter=py_info)) - pythonw_refs = [s for s in sources if is_exe(s) and has_src(path(py_info.prefix, "pythonw.exe"))(s)] - assert len(pythonw_refs) == 1 - assert "pythonw3.exe" in pythonw_refs[0].aliases - - -@pytest.mark.parametrize("py_info_name", ["cpython3_win_free_threaded"]) -def test_free_threaded_exe_naming(py_info, mock_files) -> None: - mock_files(CPYTHON3_PATH, [py_info.system_executable]) - sources = tuple(CPython3Windows.sources(interpreter=py_info)) - assert contains_exe(sources, py_info.system_executable, "python3.13t.exe") - pythonw_refs = [s for s in sources if is_exe(s) and has_src(path(py_info.prefix, "pythonw.exe"))(s)] - assert len(pythonw_refs) == 1 - assert "pythonw3.exe" in pythonw_refs[0].aliases - assert "pythonw3.13t.exe" in pythonw_refs[0].aliases - - -@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"]) -def test_pywin32_dll_exclusion(py_info, mock_files) -> None: - """Test that pywin32 DLLs are excluded from virtualenv creation.""" - # Mock pywin32 DLLs that should be excluded - pywin32_dlls = ( - path(py_info.prefix, "pywintypes39.dll"), - path(py_info.prefix, "pywintypes310.dll"), - path(py_info.prefix, "pythoncom39.dll"), - path(py_info.prefix, "pythoncom310.dll"), - ) - # Mock regular DLLs that should be included - regular_dlls = ( - path(py_info.prefix, "libcrypto-1_1.dll"), - path(py_info.prefix, "libffi-7.dll"), - ) - # Only mock the DLL files (no shim) so dll_and_pyd() method will be called - all_files = [*pywin32_dlls, *regular_dlls] - mock_files(CPYTHON3_PATH, all_files) - sources = tuple(CPython3Windows.sources(interpreter=py_info)) - - # Make sure we're in the no-shim code path - assert not CPython3Windows.has_shim(interpreter=py_info) - - # Verify pywin32 DLLs are excluded - for dll in pywin32_dlls: - assert not contains_ref(sources, dll), f"pywin32 DLL {dll} should be excluded" - - # Verify regular DLLs are included - for dll in regular_dlls: - assert contains_ref(sources, dll), f"Regular DLL {dll} should be included" diff --git a/tests/unit/create/via_global_ref/builtin/pypy/deb_pypy37.json b/tests/unit/create/via_global_ref/builtin/pypy/deb_pypy37.json deleted file mode 100644 index 4e6ca4dda..000000000 --- a/tests/unit/create/via_global_ref/builtin/pypy/deb_pypy37.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "platform": "linux", - "implementation": "PyPy", - "pypy_version_info": [7, 3, 7, "final", 0], - "version_info": { - "major": 3, - "minor": 7, - "micro": 12, - "releaselevel": "final", - "serial": 0 - }, - "architecture": 64, - "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]", - "os": "posix", - "prefix": "/usr/lib/pypy3", - "base_prefix": "/usr/lib/pypy3", - "real_prefix": null, - "base_exec_prefix": "/usr/lib/pypy3", - "exec_prefix": "/usr/lib/pypy3", - "executable": "/usr/bin/pypy3", - "original_executable": "/usr/bin/pypy3", - "system_executable": "/usr/bin/pypy3", - "has_venv": true, - "path": [ - "/usr/lib/pypy3/lib_pypy/__extensions__", - "/usr/lib/pypy3/lib_pypy", - "/usr/lib/pypy3/lib-python/3", - "/usr/lib/pypy3/lib-python/3/lib-tk", - "/usr/lib/pypy3/lib-python/3/plat-linux2", - "/usr/local/lib/pypy3.7/dist-packages", - "/usr/lib/python3/dist-packages" - ], - "file_system_encoding": "utf-8", - "stdout_encoding": "UTF-8", - "sysconfig_scheme": null, - "sysconfig_paths": { - "stdlib": "{base}/lib-python/{py_version_short}", - "platstdlib": "{base}/lib-python/{py_version_short}", - "purelib": "{base}/../../local/lib/pypy{py_version_short}/lib-python", - "platlib": "{base}/../../local/lib/pypy{py_version_short}/lib-python", - "include": "{base}/include", - "scripts": "{base}/../../local/bin", - "data": "{base}/../../local" - }, - "distutils_install": { - "purelib": "site-packages", - "platlib": "site-packages", - "headers": "include/UNKNOWN", - "scripts": "bin", - "data": "" - }, - "sysconfig": { - "makefile_filename": "/usr/lib/pypy3/lib-python/3.7/config-3.7-x86_64-linux-gnu/Makefile" - }, - "sysconfig_vars": { - "base": "/usr/lib/pypy3", - "py_version_short": "3.7", - "PYTHONFRAMEWORK": "" - }, - "system_stdlib": "/usr/lib/pypy3/lib-python/3.7", - "system_stdlib_platform": "/usr/lib/pypy3/lib-python/3.7", - "max_size": 9223372036854775807, - "_creators": null, - "free_threaded": false -} diff --git a/tests/unit/create/via_global_ref/builtin/pypy/deb_pypy38.json b/tests/unit/create/via_global_ref/builtin/pypy/deb_pypy38.json deleted file mode 100644 index 070867210..000000000 --- a/tests/unit/create/via_global_ref/builtin/pypy/deb_pypy38.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "platform": "linux", - "implementation": "PyPy", - "pypy_version_info": [7, 3, 8, "final", 0], - "version_info": { - "major": 3, - "minor": 8, - "micro": 12, - "releaselevel": "final", - "serial": 0 - }, - "architecture": 64, - "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]", - "os": "posix", - "prefix": "/usr", - "base_prefix": "/usr", - "real_prefix": null, - "base_exec_prefix": "/usr", - "exec_prefix": "/usr", - "executable": "/usr/bin/pypy3", - "original_executable": "/usr/bin/pypy3", - "system_executable": "/usr/bin/pypy3", - "has_venv": true, - "path": [ - "/usr/lib/pypy3.8", - "/usr/local/lib/pypy3.8/dist-packages", - "/usr/lib/python3/dist-packages" - ], - "file_system_encoding": "utf-8", - "stdout_encoding": "UTF-8", - "sysconfig_scheme": null, - "sysconfig_paths": { - "stdlib": "{installed_base}/lib/{implementation_lower}{py_version_short}", - "platstdlib": "{platbase}/lib/{implementation_lower}{py_version_short}", - "purelib": "{base}/local/lib/{implementation_lower}{py_version_short}/dist-packages", - "platlib": "{platbase}/local/lib/{implementation_lower}{py_version_short}/dist-packages", - "include": "{installed_base}/local/include/{implementation_lower}{py_version_short}{abiflags}", - "scripts": "{base}/local/bin", - "data": "{base}" - }, - "distutils_install": { - "purelib": "lib/pypy3.8/site-packages", - "platlib": "lib/pypy3.8/site-packages", - "headers": "include/pypy3.8/UNKNOWN", - "scripts": "bin", - "data": "" - }, - "sysconfig": { - "makefile_filename": "/usr/lib/pypy3.8/config-3.8-x86_64-linux-gnu/Makefile" - }, - "sysconfig_vars": { - "installed_base": "/usr", - "implementation_lower": "pypy", - "py_version_short": "3.8", - "platbase": "/usr", - "base": "/usr", - "abiflags": "", - "PYTHONFRAMEWORK": "" - }, - "system_stdlib": "/usr/lib/pypy3.8", - "system_stdlib_platform": "/usr/lib/pypy3.8", - "max_size": 9223372036854775807, - "_creators": null, - "free_threaded": false -} diff --git a/tests/unit/create/via_global_ref/builtin/pypy/portable_pypy38.json b/tests/unit/create/via_global_ref/builtin/pypy/portable_pypy38.json deleted file mode 100644 index 136c1b4f6..000000000 --- a/tests/unit/create/via_global_ref/builtin/pypy/portable_pypy38.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "platform": "linux", - "implementation": "PyPy", - "pypy_version_info": [7, 3, 8, "final", 0], - "version_info": { - "major": 3, - "minor": 8, - "micro": 12, - "releaselevel": "final", - "serial": 0 - }, - "architecture": 64, - "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)]", - "os": "posix", - "prefix": "/tmp/pypy3.8-v7.3.8-linux64", - "base_prefix": "/tmp/pypy3.8-v7.3.8-linux64", - "real_prefix": null, - "base_exec_prefix": "/tmp/pypy3.8-v7.3.8-linux64", - "exec_prefix": "/tmp/pypy3.8-v7.3.8-linux64", - "executable": "/tmp/pypy3.8-v7.3.8-linux64/bin/pypy", - "original_executable": "/tmp/pypy3.8-v7.3.8-linux64/bin/pypy", - "system_executable": "/tmp/pypy3.8-v7.3.8-linux64/bin/pypy", - "has_venv": true, - "path": [ - "/tmp/pypy3.8-v7.3.8-linux64/lib/pypy3.8", - "/tmp/pypy3.8-v7.3.8-linux64/lib/pypy3.8/site-packages" - ], - "file_system_encoding": "utf-8", - "stdout_encoding": "UTF-8", - "sysconfig_scheme": null, - "sysconfig_paths": { - "stdlib": "{installed_base}/lib/{implementation_lower}{py_version_short}", - "platstdlib": "{platbase}/lib/{implementation_lower}{py_version_short}", - "purelib": "{base}/lib/{implementation_lower}{py_version_short}/site-packages", - "platlib": "{platbase}/lib/{implementation_lower}{py_version_short}/site-packages", - "include": "{installed_base}/include/{implementation_lower}{py_version_short}{abiflags}", - "scripts": "{base}/bin", - "data": "{base}" - }, - "distutils_install": { - "purelib": "lib/pypy3.8/site-packages", - "platlib": "lib/pypy3.8/site-packages", - "headers": "include/pypy3.8/UNKNOWN", - "scripts": "bin", - "data": "" - }, - "sysconfig": { - "makefile_filename": "/tmp/pypy3.8-v7.3.8-linux64/lib/pypy3.8/config-3.8-x86_64-linux-gnu/Makefile" - }, - "sysconfig_vars": { - "installed_base": "/tmp/pypy3.8-v7.3.8-linux64", - "implementation_lower": "pypy", - "py_version_short": "3.8", - "platbase": "/tmp/pypy3.8-v7.3.8-linux64", - "base": "/tmp/pypy3.8-v7.3.8-linux64", - "abiflags": "", - "PYTHONFRAMEWORK": "" - }, - "system_stdlib": "/tmp/pypy3.8-v7.3.8-linux64/lib/pypy3.8", - "system_stdlib_platform": "/tmp/pypy3.8-v7.3.8-linux64/lib/pypy3.8", - "max_size": 9223372036854775807, - "_creators": null, - "free_threaded": false -} diff --git a/tests/unit/create/via_global_ref/builtin/pypy/test_pypy3.py b/tests/unit/create/via_global_ref/builtin/pypy/test_pypy3.py deleted file mode 100644 index d63f36d62..000000000 --- a/tests/unit/create/via_global_ref/builtin/pypy/test_pypy3.py +++ /dev/null @@ -1,84 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING -from unittest.mock import MagicMock - -import pytest -from testing.helpers import contains_exe, contains_ref -from testing.path import join as path - -from virtualenv.create.via_global_ref.builtin.pypy.pypy3 import PyPy3Posix -from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest - -if TYPE_CHECKING: - from pathlib import Path - - from pytest_mock import MockerFixture - -PYPY3_PATH = ( - "virtualenv.create.via_global_ref.builtin.pypy.common.Path", - "virtualenv.create.via_global_ref.builtin.pypy.pypy3.Path", -) - - -# In `PyPy3Posix.sources()` `host_lib` will be broken in Python 2 for Windows, -# so `py_file` will not be in sources. -@pytest.mark.parametrize("py_info_name", ["portable_pypy38"]) -def test_portable_pypy3_virtualenvs_get_their_libs(py_info, mock_files, mock_pypy_libs) -> None: - py_file = path(py_info.prefix, "lib/libgdbm.so.4") - mock_files(PYPY3_PATH, [py_info.system_executable, py_file]) - lib_file = path(py_info.prefix, "bin/libpypy3-c.so") - mock_pypy_libs(PyPy3Posix, [lib_file]) - sources = tuple(PyPy3Posix.sources(interpreter=py_info)) - assert len(sources) > 2 - assert contains_exe(sources, py_info.system_executable) - assert contains_ref(sources, py_file) - assert contains_ref(sources, lib_file) - - -@pytest.mark.parametrize("py_info_name", ["deb_pypy37"]) -def test_debian_pypy37_virtualenvs(py_info, mock_files, mock_pypy_libs) -> None: - # Debian's pypy3 layout, installed to /usr, before 3.8 allowed a /usr prefix - mock_files(PYPY3_PATH, [py_info.system_executable]) - lib_file = path(py_info.prefix, "bin/libpypy3-c.so") - mock_pypy_libs(PyPy3Posix, [lib_file]) - sources = tuple(PyPy3Posix.sources(interpreter=py_info)) - assert len(sources) == 2 - assert contains_exe(sources, py_info.system_executable) - assert contains_ref(sources, lib_file) - - -@pytest.mark.parametrize("py_info_name", ["deb_pypy38"]) -def test_debian_pypy38_virtualenvs_exclude_usr(py_info, mock_files, mock_pypy_libs) -> None: - mock_files(PYPY3_PATH, [py_info.system_executable, "/usr/lib/foo"]) - # libpypy3-c.so lives on the ld search path - mock_pypy_libs(PyPy3Posix, []) - sources = tuple(PyPy3Posix.sources(interpreter=py_info)) - assert len(sources) == 1 - assert contains_exe(sources, py_info.system_executable) - - -def test_pypy_portable_deps_txt(tmp_path: Path, mocker: MockerFixture) -> None: - host_lib = tmp_path / "lib" - host_lib.mkdir() - stdlib = host_lib / "pypy3.10" - stdlib.mkdir() - (host_lib / "libssl.so").touch() - (host_lib / "libcrypto.so").touch() - (host_lib / "unneeded.so").touch() - deps_file = host_lib / "PYPY_PORTABLE_DEPS.txt" - deps_file.write_text("libssl.so\nlibcrypto.so\n", encoding="utf-8") - - interpreter = MagicMock() - interpreter.system_prefix = str(tmp_path) - interpreter.system_executable = str(tmp_path / "bin" / "pypy3") - interpreter.system_stdlib = str(stdlib) - interpreter.version_info = MagicMock(major=3, minor=10) - - mocker.patch.object(PyPy3Posix, "_shared_libs", return_value=[]) - - sources = list(PyPy3Posix.sources(interpreter)) - ref_names = {s.src.name for s in sources if isinstance(s, PathRefToDest)} - assert "libssl.so" in ref_names - assert "libcrypto.so" in ref_names - assert "unneeded.so" not in ref_names diff --git a/tests/unit/create/via_global_ref/builtin/rustpython/rustpython_posix.json b/tests/unit/create/via_global_ref/builtin/rustpython/rustpython_posix.json deleted file mode 100644 index 23bde801b..000000000 --- a/tests/unit/create/via_global_ref/builtin/rustpython/rustpython_posix.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "platform": "linux", - "implementation": "RustPython", - "version_info": { - "major": 3, - "minor": 14, - "micro": 0, - "releaselevel": "alpha", - "serial": 0 - }, - "architecture": 64, - "version": "3.14.0alpha (default, Nov 10 2025, 09:14:50)\n[RustPython 0.4.0 with rustc 1.83.0 (90b35a623 2024-11-26)]", - "os": "posix", - "prefix": "/usr/local", - "base_prefix": "/usr/local", - "real_prefix": null, - "base_exec_prefix": "/usr/local", - "exec_prefix": "/usr/local", - "executable": "/usr/local/bin/rustpython", - "original_executable": "/usr/local/bin/rustpython", - "system_executable": "/usr/local/bin/rustpython", - "has_venv": true, - "path": [ - "/usr/local/lib/python3.14", - "/usr/local/lib/python3.14/site-packages" - ], - "file_system_encoding": "utf-8", - "stdout_encoding": "UTF-8", - "sysconfig_scheme": null, - "sysconfig_paths": { - "stdlib": "{installed_base}/lib/python{py_version_short}", - "platstdlib": "{platbase}/lib/python{py_version_short}", - "purelib": "{base}/lib/python{py_version_short}/site-packages", - "platlib": "{platbase}/lib/python{py_version_short}/site-packages", - "include": "{installed_base}/include/python{py_version_short}{abiflags}", - "scripts": "{base}/bin", - "data": "{base}" - }, - "distutils_install": { - "purelib": "lib/python3.14/site-packages", - "platlib": "lib/python3.14/site-packages", - "headers": "include/python3.14/UNKNOWN", - "scripts": "bin", - "data": "" - }, - "sysconfig": {}, - "sysconfig_vars": { - "installed_base": "/usr/local", - "implementation_lower": "rustpython", - "py_version_short": "3.14", - "platbase": "/usr/local", - "base": "/usr/local", - "abiflags": "t", - "PYTHONFRAMEWORK": "" - }, - "system_stdlib": "/usr/local/lib/python3.14", - "system_stdlib_platform": "/usr/local/lib/python3.14", - "max_size": 9223372036854775807, - "_creators": null, - "free_threaded": false -} diff --git a/tests/unit/create/via_global_ref/builtin/rustpython/rustpython_windows.json b/tests/unit/create/via_global_ref/builtin/rustpython/rustpython_windows.json deleted file mode 100644 index f87b6dcfe..000000000 --- a/tests/unit/create/via_global_ref/builtin/rustpython/rustpython_windows.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "platform": "win32", - "implementation": "RustPython", - "version_info": { - "major": 3, - "minor": 14, - "micro": 0, - "releaselevel": "alpha", - "serial": 0 - }, - "architecture": 64, - "version": "3.14.0alpha (default, Nov 10 2025, 09:14:50)\n[RustPython 0.4.0 with rustc 1.83.0 (90b35a623 2024-11-26) MSC v.1929 64 bit (AMD64)]", - "os": "nt", - "prefix": "C:\\RustPython", - "base_prefix": "C:\\RustPython", - "real_prefix": null, - "base_exec_prefix": "C:\\RustPython", - "exec_prefix": "C:\\RustPython", - "executable": "C:\\RustPython\\rustpython.exe", - "original_executable": "C:\\RustPython\\rustpython.exe", - "system_executable": "C:\\RustPython\\rustpython.exe", - "has_venv": true, - "path": ["C:\\RustPython\\Lib", "C:\\RustPython\\Lib\\site-packages"], - "file_system_encoding": "utf-8", - "stdout_encoding": "UTF-8", - "sysconfig_scheme": null, - "sysconfig_paths": { - "stdlib": "{installed_base}\\Lib", - "platstdlib": "{platbase}\\Lib", - "purelib": "{base}\\Lib\\site-packages", - "platlib": "{platbase}\\Lib\\site-packages", - "include": "{installed_base}\\Include", - "scripts": "{base}\\Scripts", - "data": "{base}" - }, - "distutils_install": { - "purelib": "Lib\\site-packages", - "platlib": "Lib\\site-packages", - "headers": "Include\\UNKNOWN", - "scripts": "Scripts", - "data": "" - }, - "sysconfig": {}, - "sysconfig_vars": { - "installed_base": "C:\\RustPython", - "implementation_lower": "rustpython", - "py_version_short": "3.14", - "platbase": "C:\\RustPython", - "base": "C:\\RustPython", - "abiflags": "t", - "PYTHONFRAMEWORK": "" - }, - "system_stdlib": "C:\\RustPython\\Lib", - "system_stdlib_platform": "C:\\RustPython\\Lib", - "max_size": 9223372036854775807, - "_creators": null, - "free_threaded": false -} diff --git a/tests/unit/create/via_global_ref/builtin/rustpython/test_rustpython.py b/tests/unit/create/via_global_ref/builtin/rustpython/test_rustpython.py deleted file mode 100644 index 584b06945..000000000 --- a/tests/unit/create/via_global_ref/builtin/rustpython/test_rustpython.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import annotations - -from unittest.mock import MagicMock - -import pytest -from testing.helpers import contains_exe - -from virtualenv.create.via_global_ref.builtin.rustpython import RustPythonPosix, RustPythonWindows - - -@pytest.mark.parametrize("py_info_name", ["rustpython_posix"]) -def test_can_describe_rustpython_posix(py_info: MagicMock) -> None: - assert RustPythonPosix.can_describe(py_info) - - -@pytest.mark.parametrize("py_info_name", ["rustpython_windows"]) -def test_can_describe_rustpython_windows(py_info: MagicMock) -> None: - assert RustPythonWindows.can_describe(py_info) - - -def test_can_describe_rejects_cpython() -> None: - interpreter = MagicMock() - interpreter.implementation = "CPython" - interpreter.os = "posix" - assert not RustPythonPosix.can_describe(interpreter) - interpreter.os = "nt" - assert not RustPythonWindows.can_describe(interpreter) - - -@pytest.mark.parametrize("py_info_name", ["rustpython_posix"]) -def test_sources_posix(py_info: MagicMock, mock_files: object) -> None: - mock_files(("virtualenv.create.via_global_ref.builtin.rustpython.Path",), [py_info.system_executable]) - sources = list(RustPythonPosix.sources(interpreter=py_info)) - assert len(sources) == 1 - assert contains_exe(sources, py_info.system_executable) - - -@pytest.mark.parametrize("py_info_name", ["rustpython_windows"]) -def test_sources_windows(py_info: MagicMock, mock_files: object) -> None: - mock_files(("virtualenv.create.via_global_ref.builtin.rustpython.Path",), [py_info.system_executable]) - sources = list(RustPythonWindows.sources(interpreter=py_info)) - assert len(sources) == 1 - assert contains_exe(sources, py_info.system_executable) - - -def test_exe_names() -> None: - interpreter = MagicMock() - interpreter.version_info = MagicMock(major=3, minor=14) - names = RustPythonPosix.exe_names(interpreter) - assert names == {"rustpython", "python", "python3", "python3.14"} diff --git a/tests/unit/create/via_global_ref/builtin/testing/__init__.py b/tests/unit/create/via_global_ref/builtin/testing/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/unit/create/via_global_ref/builtin/testing/helpers.py b/tests/unit/create/via_global_ref/builtin/testing/helpers.py deleted file mode 100644 index e55c8d025..000000000 --- a/tests/unit/create/via_global_ref/builtin/testing/helpers.py +++ /dev/null @@ -1,40 +0,0 @@ -from __future__ import annotations - -from functools import reduce -from pathlib import Path - -from virtualenv.create.via_global_ref.builtin.ref import ExePathRefToDest, PathRef - - -def is_ref(source): - return isinstance(source, PathRef) - - -def is_exe(source): - return type(source) is ExePathRefToDest - - -def has_src(src): - return lambda ref: ref.src.as_posix() == Path(src).as_posix() - - -def has_target(target): - return lambda ref: ref.base == target - - -def apply_filter(values, function): - return filter(function, values) - - -def filterby(filters, sources): - return reduce(apply_filter, filters, sources) - - -def contains_exe(sources, src, target=None): - filters = is_exe, has_src(src), target and has_target(target) - return any(filterby(filters, sources)) - - -def contains_ref(sources, src): - filters = is_ref, has_src(src) - return any(filterby(filters, sources)) diff --git a/tests/unit/create/via_global_ref/builtin/testing/path.py b/tests/unit/create/via_global_ref/builtin/testing/path.py deleted file mode 100644 index eeeebcf07..000000000 --- a/tests/unit/create/via_global_ref/builtin/testing/path.py +++ /dev/null @@ -1,97 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from itertools import chain -from operator import attrgetter as attr -from pathlib import Path - - -def is_name(path): - return str(path) == path.name - - -class FakeDataABC(ABC): - """Provides data to mock the `Path`""" - - @property - @abstractmethod - def filelist(self): - """To mock a dir, just mock any child file.""" - msg = "Collection of (str) file paths to mock" - raise NotImplementedError(msg) - - @property - def fake_files(self): - return map(type(self), self.filelist) - - @property - def fake_dirs(self): - return set(chain(*map(attr("parents"), self.fake_files))) - - @property - def contained_fake_names(self): - return filter(is_name, self.fake_content) - - @property - def fake_content(self): - return filter(None, map(self.fake_child, self.fake_files)) - - def fake_child(self, path): - try: - return path.relative_to(self) - except ValueError: - return None - - -class PathMockABC(FakeDataABC, Path): - """Mocks the behavior of `Path`""" - - _flavour = getattr(Path(), "_flavour", None) - if hasattr(_flavour, "altsep"): - # Allows to pass some tests for Windows via PosixPath. - _flavour.altsep = _flavour.altsep or "\\" - - # Python 3.13 renamed _flavour to parser - parser = getattr(Path(), "parser", None) - if hasattr(parser, "altsep"): - parser.altsep = parser.altsep or "\\" - - def exists(self): - return self.is_file() or self.is_dir() - - def is_file(self): - return self in self.fake_files - - def is_dir(self): - return self in self.fake_dirs - - def resolve(self): - return self - - def iterdir(self): - if not self.is_dir(): - msg = f"No such mocked dir: '{self}'" - raise FileNotFoundError(msg) - yield from map(self.joinpath, self.contained_fake_names) - - -def MetaPathMock(filelist): # noqa: N802 - """Metaclass that creates a `PathMock` class with the `filelist` defined.""" - return type("PathMock", (PathMockABC,), {"filelist": filelist}) - - -def mock_files(mocker, pathlist, filelist) -> None: - PathMock = MetaPathMock(set(filelist)) # noqa: N806 - for path in pathlist: - mocker.patch(path, PathMock) - - -def mock_pypy_libs(mocker, pypy_creator_cls, libs) -> None: - paths = tuple(set(map(Path, libs))) - mocker.patch.object(pypy_creator_cls, "_shared_libs", return_value=paths) - - -def join(*chunks): - line = "".join(chunks) - sep = ("\\" in line and "\\") or ("/" in line and "/") or "/" - return sep.join(chunks) diff --git a/tests/unit/create/via_global_ref/builtin/testing/py_info.py b/tests/unit/create/via_global_ref/builtin/testing/py_info.py deleted file mode 100644 index 99fc13e71..000000000 --- a/tests/unit/create/via_global_ref/builtin/testing/py_info.py +++ /dev/null @@ -1,21 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -from python_discovery import PythonInfo - - -def fixture_file(fixture_name): - file_mask = f"*{fixture_name}.json" - files = Path(__file__).parent.parent.rglob(file_mask) - try: - return next(files) - except StopIteration as exc: - # Fixture file was not found in the testing root and its subdirs. - error = FileNotFoundError - raise error(file_mask) from exc - - -def read_fixture(fixture_name): - fixture_json = fixture_file(fixture_name).read_text(encoding="utf-8") - return PythonInfo.from_json(fixture_json) diff --git a/tests/unit/create/via_global_ref/greet/greet2.c b/tests/unit/create/via_global_ref/greet/greet2.c deleted file mode 100644 index 7dc421c61..000000000 --- a/tests/unit/create/via_global_ref/greet/greet2.c +++ /dev/null @@ -1,30 +0,0 @@ -#include -#include - -static PyObject * greet(PyObject * self, PyObject * args) { - const char * name; - if (!PyArg_ParseTuple(args, "s", & name)) { - return NULL; - } - printf("Hello %s!\n", name); - Py_RETURN_NONE; -} - -static PyMethodDef GreetMethods[] = { - { - "greet", - greet, - METH_VARARGS, - "Greet an entity." - }, - { - NULL, - NULL, - 0, - NULL - } -}; - -PyMODINIT_FUNC initgreet(void) { - (void) Py_InitModule("greet", GreetMethods); -} diff --git a/tests/unit/create/via_global_ref/greet/greet3.c b/tests/unit/create/via_global_ref/greet/greet3.c deleted file mode 100644 index 3ec017d2b..000000000 --- a/tests/unit/create/via_global_ref/greet/greet3.c +++ /dev/null @@ -1,38 +0,0 @@ -#include -#include - -static PyObject * greet(PyObject * self, PyObject * args) { - const char * name; - if (!PyArg_ParseTuple(args, "s", & name)) { - return NULL; - } - printf("Hello %s!\n", name); - Py_RETURN_NONE; -} - -static PyMethodDef GreetMethods[] = { - { - "greet", - greet, - METH_VARARGS, - "Greet an entity." - }, - { - NULL, - NULL, - 0, - NULL - } -}; - -static struct PyModuleDef greet_definition = { - PyModuleDef_HEAD_INIT, - "greet", - "A Python module that prints 'greet world' from C code.", - -1, - GreetMethods -}; - -PyMODINIT_FUNC PyInit_greet(void) { - return PyModule_Create( & greet_definition); -} diff --git a/tests/unit/create/via_global_ref/greet/setup.py b/tests/unit/create/via_global_ref/greet/setup.py deleted file mode 100644 index 2965915b4..000000000 --- a/tests/unit/create/via_global_ref/greet/setup.py +++ /dev/null @@ -1,16 +0,0 @@ -from __future__ import annotations - -import sys - -from setuptools import Extension, setup - -setup( - name="greet", # package name - version="1.0", # package version - ext_modules=[ - Extension( - "greet", - [f"greet{sys.version_info[0]}.c"], # extension to package - ), # C code to compile to run as extension - ], -) diff --git a/tests/unit/create/via_global_ref/test_api.py b/tests/unit/create/via_global_ref/test_api.py deleted file mode 100644 index aa6dad31f..000000000 --- a/tests/unit/create/via_global_ref/test_api.py +++ /dev/null @@ -1,8 +0,0 @@ -from __future__ import annotations - -from virtualenv.create.via_global_ref import api - - -def test_can_symlink_when_symlinks_not_enabled(mocker) -> None: - mocker.patch.object(api, "fs_supports_symlink", return_value=False) - assert api.ViaGlobalRefMeta().can_symlink is False diff --git a/tests/unit/create/via_global_ref/test_build_c_ext.py b/tests/unit/create/via_global_ref/test_build_c_ext.py deleted file mode 100644 index 97cf8df7c..000000000 --- a/tests/unit/create/via_global_ref/test_build_c_ext.py +++ /dev/null @@ -1,70 +0,0 @@ -from __future__ import annotations - -import os -import shutil -import subprocess -from pathlib import Path -from subprocess import Popen - -import pytest -from python_discovery import PythonInfo - -from virtualenv.run import cli_run -from virtualenv.run.plugin.creators import CreatorSelector -from virtualenv.seed.wheels.embed import BUNDLE_FOLDER as EMBED_WHEEL_DIR - -CURRENT = PythonInfo.current_system() -CREATOR_CLASSES = CreatorSelector.for_interpreter(CURRENT).key_to_class - - -def builtin_shows_marker_missing() -> bool: - builtin_classs = CREATOR_CLASSES.get("builtin") - if builtin_classs is None: - return False - host_include_marker = getattr(builtin_classs, "host_include_marker", None) - if host_include_marker is None: - return False - marker = host_include_marker(CURRENT) - return not marker.exists() - - -@pytest.mark.slow -@pytest.mark.xfail( - condition=bool(os.environ.get("CI_RUN")), - strict=False, - reason="did not manage to setup CI to run with VC 14.1 C++ compiler, but passes locally", -) -@pytest.mark.skipif( - not Path(CURRENT.system_include).exists() and not builtin_shows_marker_missing(), - reason="Building C-Extensions requires header files with host python", -) -@pytest.mark.parametrize("creator", [i for i in CREATOR_CLASSES if i != "builtin"]) -def test_can_build_c_extensions(creator, tmp_path, coverage_env) -> None: - env, greet = tmp_path / "env", str(tmp_path / "greet") - shutil.copytree(str(Path(__file__).parent.resolve() / "greet"), greet) - session = cli_run(["--creator", creator, "--seeder", "app-data", str(env), "-vvv"]) - coverage_env() - cmd = [ - str(session.creator.script("pip")), - "install", - "--no-index", - "--find-links", - str(EMBED_WHEEL_DIR), - "--no-deps", - "--disable-pip-version-check", - "-vvv", - greet, - ] - process = Popen(cmd) - process.communicate() - assert process.returncode == 0 - - process = Popen( - [str(session.creator.exe), "-c", "import greet; greet.greet('World')"], - universal_newlines=True, - stdout=subprocess.PIPE, - encoding="utf-8", - ) - out, _ = process.communicate() - assert process.returncode == 0 - assert out == "Hello World!\n" diff --git a/tests/unit/create/via_global_ref/test_race_condition.py b/tests/unit/create/via_global_ref/test_race_condition.py deleted file mode 100644 index 1b3dc9886..000000000 --- a/tests/unit/create/via_global_ref/test_race_condition.py +++ /dev/null @@ -1,73 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - - -def test_virtualenv_py_race_condition_find_spec(tmp_path) -> None: - """Test that _Finder.find_spec handles NameError gracefully when _DISTUTILS_PATCH is not defined.""" - # Create a temporary file with partial _virtualenv.py content (simulating race condition) - venv_file = tmp_path / "_virtualenv_test.py" - - # Write a partial version of _virtualenv.py that has _Finder but not _DISTUTILS_PATCH - # This simulates the state during a race condition where the file is being rewritten - helper_file = Path(__file__).parent / "_test_race_condition_helper.py" - partial_content = helper_file.read_text(encoding="utf-8") - - venv_file.write_text(partial_content, encoding="utf-8") - - sys.path.insert(0, str(tmp_path)) - try: - import _virtualenv_test # noqa: PLC0415 - - finder = _virtualenv_test.finder - - # Try to call find_spec - this should not raise NameError - result = finder.find_spec("distutils.dist", None) - assert result is None, "find_spec should return None when _DISTUTILS_PATCH is not defined" - - # Create a mock module object - class MockModule: - __name__ = "distutils.dist" - - # Try to call exec_module - this should not raise NameError - def mock_old_exec(_x) -> None: - pass - - finder.exec_module(mock_old_exec, MockModule()) - - # Try to call load_module - this should not raise NameError - def mock_old_load(_name): - return MockModule() - - result = finder.load_module(mock_old_load, "distutils.dist") - assert result.__name__ == "distutils.dist" - - finally: - sys.path.remove(str(tmp_path)) - if "_virtualenv_test" in sys.modules: - del sys.modules["_virtualenv_test"] - - -def test_virtualenv_py_normal_operation() -> None: - """Test that the fix doesn't break normal operation when _DISTUTILS_PATCH is defined.""" - # Read the actual _virtualenv.py file - virtualenv_py_path = ( - Path(__file__).parent.parent.parent.parent.parent - / "src" - / "virtualenv" - / "create" - / "via_global_ref" - / "_virtualenv.py" - ) - - if not virtualenv_py_path.exists(): - return # Skip if we can't find the file - - content = virtualenv_py_path.read_text(encoding="utf-8") - - # Verify the fix is present - assert "try:" in content - assert "distutils_patch = _DISTUTILS_PATCH" in content - assert "except NameError:" in content - assert "return None" in content or "return" in content diff --git a/tests/unit/discovery/test_discovery.py b/tests/unit/discovery/test_discovery.py deleted file mode 100644 index b0c920bb3..000000000 --- a/tests/unit/discovery/test_discovery.py +++ /dev/null @@ -1,221 +0,0 @@ -from __future__ import annotations - -import logging -import os -import subprocess -import sys -from argparse import Namespace -from pathlib import Path - -import pytest -from python_discovery import PythonInfo - -from virtualenv.discovery.builtin import Builtin, get_interpreter -from virtualenv.info import IS_WIN - - -def test_relative_path(session_app_data, monkeypatch) -> None: - sys_executable = Path(PythonInfo.current_system(session_app_data).system_executable) - cwd = sys_executable.parents[1] - monkeypatch.chdir(str(cwd)) - relative = str(sys_executable.relative_to(cwd)) - result = get_interpreter(relative, [], session_app_data) - assert result is not None - - -def test_discovery_fallback_fail(session_app_data, caplog) -> None: - caplog.set_level(logging.DEBUG) - builtin = Builtin( - Namespace(app_data=session_app_data, try_first_with=[], python=["magic-one", "magic-two"], env=os.environ), - ) - - result = builtin.run() - assert result is None - - assert "accepted" not in caplog.text - - -def test_discovery_fallback_ok(session_app_data, caplog) -> None: - caplog.set_level(logging.DEBUG) - builtin = Builtin( - Namespace(app_data=session_app_data, try_first_with=[], python=["magic-one", sys.executable], env=os.environ), - ) - - result = builtin.run() - assert result is not None, caplog.text - assert result.executable == sys.executable, caplog.text - - assert "accepted" in caplog.text - - -@pytest.fixture -def mock_get_interpreter(mocker): - return mocker.patch( - "virtualenv.discovery.builtin.get_interpreter", - lambda key, *_args, **_kwargs: getattr(mocker.sentinel, key), - ) - - -@pytest.mark.usefixtures("mock_get_interpreter") -def test_returns_first_python_specified_when_only_env_var_one_is_specified( - mocker, monkeypatch, session_app_data -) -> None: - monkeypatch.setenv("VIRTUALENV_PYTHON", "python_from_env_var") - builtin = Builtin( - Namespace(app_data=session_app_data, try_first_with=[], python=["python_from_env_var"], env=os.environ), - ) - - result = builtin.run() - - assert result == mocker.sentinel.python_from_env_var - - -@pytest.mark.usefixtures("mock_get_interpreter") -def test_returns_second_python_specified_when_more_than_one_is_specified_and_env_var_is_specified( - mocker, monkeypatch, session_app_data -) -> None: - monkeypatch.setenv("VIRTUALENV_PYTHON", "python_from_env_var") - builtin = Builtin( - Namespace( - app_data=session_app_data, - try_first_with=[], - python=["python_from_env_var", "python_from_cli"], - env=os.environ, - ), - ) - - result = builtin.run() - - assert result == mocker.sentinel.python_from_cli - - -def test_discovery_absolute_path_with_try_first(tmp_path, session_app_data) -> None: - good_env = tmp_path / "good" - bad_env = tmp_path / "bad" - - subprocess.check_call([sys.executable, "-m", "virtualenv", str(good_env)]) - subprocess.check_call([sys.executable, "-m", "virtualenv", str(bad_env)]) - - scripts_dir = "Scripts" if IS_WIN else "bin" - exe_name = "python.exe" if IS_WIN else "python" - good_exe = good_env / scripts_dir / exe_name - bad_exe = bad_env / scripts_dir / exe_name - - interpreter = get_interpreter( - str(good_exe), - try_first_with=[str(bad_exe)], - app_data=session_app_data, - ) - - assert interpreter is not None - assert Path(interpreter.executable) == good_exe - - -def test_absolute_path_does_not_exist(tmp_path) -> None: - """Test that virtualenv does not fail when an absolute path that does not exist is provided.""" - command = [ - sys.executable, - "-m", - "virtualenv", - "-p", - "/this/path/does/not/exist", - "-p", - sys.executable, - str(tmp_path / "dest"), - ] - - process = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - encoding="utf-8", - ) - - assert process.returncode == 0, process.stderr - - -def test_absolute_path_does_not_exist_fails(tmp_path) -> None: - """Test that virtualenv fails when a single absolute path that does not exist is provided.""" - command = [ - sys.executable, - "-m", - "virtualenv", - "-p", - "/this/path/does/not/exist", - str(tmp_path / "dest"), - ] - - process = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - encoding="utf-8", - ) - - assert process.returncode != 0, process.stderr - - -@pytest.mark.usefixtures("mock_get_interpreter") -def test_returns_first_python_specified_when_no_env_var_is_specified(mocker, monkeypatch, session_app_data) -> None: - monkeypatch.delenv("VIRTUALENV_PYTHON", raising=False) - builtin = Builtin( - Namespace(app_data=session_app_data, try_first_with=[], python=["python_from_cli"], env=os.environ), - ) - - result = builtin.run() - - assert result == mocker.sentinel.python_from_cli - - -def test_discovery_via_version_specifier(session_app_data) -> None: - """Test that version specifiers like >=3.11 work correctly through the virtualenv wrapper.""" - current = PythonInfo.current_system(session_app_data) - major, minor = current.version_info.major, current.version_info.minor - - spec = f">={major}.{minor}" - interpreter = get_interpreter(spec, [], session_app_data) - assert interpreter is not None - assert interpreter.version_info.major == major - assert interpreter.version_info.minor >= minor - - spec = f">={major}.{minor},<{major}.{minor + 10}" - interpreter = get_interpreter(spec, [], session_app_data) - assert interpreter is not None - assert interpreter.version_info.major == major - assert minor <= interpreter.version_info.minor < minor + 10 - - spec = f"cpython>={major}.{minor}" - interpreter = get_interpreter(spec, [], session_app_data) - if current.implementation == "CPython": - assert interpreter is not None - assert interpreter.implementation == "CPython" - - -def test_invalid_discovery_via_env_var(monkeypatch, tmp_path) -> None: - """When VIRTUALENV_DISCOVERY is set to an unavailable plugin, raise a clear error instead of KeyError.""" - monkeypatch.setenv("VIRTUALENV_DISCOVERY", "nonexistent_plugin") - process = subprocess.run( - [sys.executable, "-m", "virtualenv", str(tmp_path / "env")], - capture_output=True, - text=True, - check=False, - encoding="utf-8", - ) - assert process.returncode != 0 - output = process.stdout + process.stderr - assert "nonexistent_plugin" in output - assert "is not available" in output - assert "KeyError" not in output - - -def test_invalid_discovery_via_env_var_unit(monkeypatch) -> None: - """Unit test: get_discover raises RuntimeError with helpful message for unknown discovery method.""" - from virtualenv.config.cli.parser import VirtualEnvConfigParser # noqa: PLC0415 - from virtualenv.run.plugin.discovery import get_discover # noqa: PLC0415 - - monkeypatch.setenv("VIRTUALENV_DISCOVERY", "nonexistent_plugin") - parser = VirtualEnvConfigParser() - with pytest.raises(RuntimeError, match=r"nonexistent_plugin.*is not available"): - get_discover(parser, []) diff --git a/tests/unit/seed/embed/test_base_embed.py b/tests/unit/seed/embed/test_base_embed.py deleted file mode 100644 index 4dd83756a..000000000 --- a/tests/unit/seed/embed/test_base_embed.py +++ /dev/null @@ -1,65 +0,0 @@ -from __future__ import annotations - -import sys -from typing import TYPE_CHECKING - -import pytest - -from virtualenv.run import session_via_cli - -if TYPE_CHECKING: - from pathlib import Path - - -@pytest.mark.parametrize( - ("args", "download"), - [([], False), (["--no-download"], False), (["--never-download"], False), (["--download"], True)], -) -def test_download_cli_flag(args, download, tmp_path) -> None: - session = session_via_cli([*args, str(tmp_path)]) - assert session.seeder.download is download - - -@pytest.mark.skipif(sys.version_info[:2] == (3, 8), reason="We still bundle wheel for Python 3.8") -@pytest.mark.parametrize("flag", ["--no-wheel", "--wheel=none", "--wheel=embed", "--wheel=bundle"]) -def test_wheel_cli_flags_do_nothing(tmp_path, flag) -> None: - session = session_via_cli([flag, str(tmp_path)]) - if sys.version_info[:2] >= (3, 12): - expected = {"pip": "bundle"} - else: - expected = {"pip": "bundle", "setuptools": "bundle"} - assert session.seeder.distribution_to_versions() == expected - - -@pytest.mark.skipif(sys.version_info[:2] == (3, 8), reason="We still bundle wheel for Python 3.8") -@pytest.mark.parametrize("flag", ["--no-wheel", "--wheel=none", "--wheel=embed", "--wheel=bundle"]) -def test_wheel_cli_flags_warn(tmp_path, flag, capsys) -> None: - session_via_cli([flag, str(tmp_path)]) - out, err = capsys.readouterr() - assert "The --no-wheel and --wheel options are deprecated." in out + err - - -@pytest.mark.skipif(sys.version_info[:2] == (3, 8), reason="We still bundle wheel for Python 3.8") -def test_unused_wheel_cli_flags_dont_warn(tmp_path, capsys) -> None: - session_via_cli([str(tmp_path)]) - out, err = capsys.readouterr() - assert "The --no-wheel and --wheel options are deprecated." not in out + err - - -@pytest.mark.skipif(sys.version_info[:2] != (3, 8), reason="We only bundle wheel for Python 3.8") -@pytest.mark.parametrize("flag", ["--no-wheel", "--wheel=none", "--wheel=embed", "--wheel=bundle"]) -def test_wheel_cli_flags_dont_warn_on_38(tmp_path, flag, capsys) -> None: - session_via_cli([flag, str(tmp_path)]) - out, err = capsys.readouterr() - assert "The --no-wheel and --wheel options are deprecated." not in out + err - - -def test_embed_wheel_versions(tmp_path: Path) -> None: - session = session_via_cli([str(tmp_path)]) - if sys.version_info[:2] >= (3, 12): - expected = {"pip": "bundle"} - elif sys.version_info[:2] >= (3, 9): - expected = {"pip": "bundle", "setuptools": "bundle"} - else: - expected = {"pip": "bundle", "setuptools": "bundle", "wheel": "bundle"} - assert session.seeder.distribution_to_versions() == expected diff --git a/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py b/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py deleted file mode 100644 index 502f32a60..000000000 --- a/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py +++ /dev/null @@ -1,264 +0,0 @@ -from __future__ import annotations - -import contextlib -import os -import sys -from stat import S_IWGRP, S_IWOTH, S_IWUSR -from subprocess import Popen, check_call -from threading import Thread -from typing import TYPE_CHECKING - -import pytest -from python_discovery import PythonInfo -from python_discovery import _cached_py_info as cached_py_info - -from virtualenv.info import fs_supports_symlink -from virtualenv.run import cli_run -from virtualenv.seed.wheels.embed import BUNDLE_FOLDER, BUNDLE_SUPPORT -from virtualenv.util.path import safe_delete - -if TYPE_CHECKING: - from pathlib import Path - - from pytest_mock import MockerFixture - - -@pytest.mark.slow -@pytest.mark.parametrize("copies", [False, True] if fs_supports_symlink() else [True]) -def test_seed_link_via_app_data(tmp_path, coverage_env, current_fastest, copies, for_py_version) -> None: # noqa: PLR0915 - current = PythonInfo.current_system() - bundle_ver = BUNDLE_SUPPORT[current.version_release_str] - create_cmd = [ - str(tmp_path / "en v"), # space in the name to ensure generated scripts work when path has space - "--no-periodic-update", - "--seeder", - "app-data", - "--extra-search-dir", - str(BUNDLE_FOLDER), - "--download", - "--pip", - bundle_ver["pip"].split("-")[1], - "--setuptools", - bundle_ver["setuptools"].split("-")[1], - "--reset-app-data", - "--creator", - current_fastest, - "-vv", - ] - if for_py_version == "3.8": - create_cmd += ["--wheel", bundle_ver["wheel"].split("-")[1]] - if not copies: - create_cmd.append("--symlink-app-data") - result = cli_run(create_cmd) - coverage_env() - assert result - - # uninstalling pip/setuptools now should leave us with a ensure_safe_to_do env - site_package = result.creator.purelib - pip = site_package / "pip" - setuptools = site_package / "setuptools" - - files_post_first_create = set(site_package.iterdir()) - assert pip in files_post_first_create - assert setuptools in files_post_first_create - for pip_exe in [ - result.creator.script_dir / f"pip{suffix}{result.creator.exe.suffix}" - for suffix in ( - "", - f"{current.version_info.major}", - f"{current.version_info.major}.{current.version_info.minor}", - f"-{current.version_info.major}.{current.version_info.minor}", - ) - ]: - assert pip_exe.exists() - process = Popen([str(pip_exe), "--version", "--disable-pip-version-check"]) - _, __ = process.communicate() - assert not process.returncode - - remove_cmd = [ - str(result.creator.script("pip")), - "--verbose", - "--disable-pip-version-check", - "uninstall", - "-y", - "setuptools", - ] - process = Popen(remove_cmd) - _, __ = process.communicate() - assert not process.returncode - assert site_package.exists() - - files_post_first_uninstall = set(site_package.iterdir()) - assert pip in files_post_first_uninstall - assert setuptools not in files_post_first_uninstall - - # install a different setuptools to test that virtualenv removes this before installing new - version = f"setuptools<{bundle_ver['setuptools'].split('-')[1]}" - install_cmd = [str(result.creator.script("pip")), "--verbose", "--disable-pip-version-check", "install", version] - process = Popen(install_cmd) - process.communicate() - assert not process.returncode - assert site_package.exists() - files_post_downgrade = set(site_package.iterdir()) - assert setuptools in files_post_downgrade - - # check we can run it again and will work - checks both overwrite and reuse cache - result = cli_run(create_cmd) - coverage_env() - assert result - files_post_second_create = set(site_package.iterdir()) - assert files_post_first_create == files_post_second_create - - # Windows does not allow removing a executable while running it, so when uninstalling pip we need to do it via - # python -m pip - remove_cmd = [str(result.creator.exe), "-m", "pip", *remove_cmd[1:]] - process = Popen([*remove_cmd, "pip", "wheel"]) - _, __ = process.communicate() - assert not process.returncode - # pip is greedy here, removing all packages removes the site-package too - if site_package.exists(): - purelib = result.creator.purelib - patch_files = {purelib / f"{'_virtualenv'}.{i}" for i in ("py", "pyc", "pth")} - patch_files.add(purelib / "__pycache__") - post_run = set(site_package.iterdir()) - patch_files - assert not post_run, "\n".join(str(i) for i in post_run) - - -@contextlib.contextmanager -def read_only_dir(d): - write = S_IWUSR | S_IWGRP | S_IWOTH - for root, _, filenames in os.walk(str(d)): - os.chmod(root, os.stat(root).st_mode & ~write) - for filename in filenames: - name = os.path.join(root, filename) - os.chmod(name, os.stat(name).st_mode & ~write) - try: - yield - finally: - for root, _, filenames in os.walk(str(d)): - os.chmod(root, os.stat(root).st_mode | write) - for filename in filenames: - name = os.path.join(root, filename) - os.chmod(name, os.stat(name).st_mode | write) - - -@pytest.fixture -def read_only_app_data(temp_app_data): - temp_app_data.mkdir() - with read_only_dir(temp_app_data): - yield temp_app_data - - -@pytest.mark.slow -@pytest.mark.skipif(sys.platform == "win32", reason="Windows only applies R/O to files") -@pytest.mark.usefixtures("read_only_app_data") -def test_base_bootstrap_link_via_app_data_not_writable(tmp_path, current_fastest) -> None: - dest = tmp_path / "venv" - result = cli_run(["--seeder", "app-data", "--creator", current_fastest, "-vv", str(dest)]) - assert result - - -@pytest.mark.slow -@pytest.mark.skipif(sys.platform == "win32", reason="Windows only applies R/O to files") -def test_populated_read_only_cache_and_symlinked_app_data(tmp_path, current_fastest, temp_app_data) -> None: - dest = tmp_path / "venv" - cmd = [ - "--seeder", - "app-data", - "--creator", - current_fastest, - "--symlink-app-data", - "-vv", - str(dest), - ] - - assert cli_run(cmd) - check_call((str(dest.joinpath("bin/python")), "-c", "import pip")) - - cached_py_info._CACHE.clear() # noqa: SLF001 # necessary to re-trigger py info discovery - safe_delete(dest) - - # should succeed with special flag when read-only - with read_only_dir(temp_app_data): - assert cli_run(["--read-only-app-data", *cmd]) - check_call((str(dest.joinpath("bin/python")), "-c", "import pip")) - - -@pytest.mark.slow -@pytest.mark.skipif(sys.platform == "win32", reason="Windows only applies R/O to files") -def test_populated_read_only_cache_and_copied_app_data(tmp_path, current_fastest, temp_app_data) -> None: - dest = tmp_path / "venv" - cmd = [ - "--seeder", - "app-data", - "--creator", - current_fastest, - "-vv", - "-p", - "python", - str(dest), - ] - - assert cli_run(cmd) - - cached_py_info._CACHE.clear() # noqa: SLF001 # necessary to re-trigger py info discovery - safe_delete(dest) - - # should succeed with special flag when read-only - with read_only_dir(temp_app_data): - assert cli_run(["--read-only-app-data", *cmd]) - - -@pytest.mark.slow -@pytest.mark.parametrize("pkg", ["pip", "setuptools", "wheel"]) -@pytest.mark.usefixtures("session_app_data", "current_fastest", "coverage_env") -def test_base_bootstrap_link_via_app_data_no(tmp_path, pkg, for_py_version) -> None: - if for_py_version != "3.8" and pkg == "wheel": - msg = "wheel isn't installed on Python > 3.8" - raise pytest.skip(msg) - create_cmd = [str(tmp_path), "--seeder", "app-data", f"--no-{pkg}", "--setuptools", "bundle"] - if for_py_version == "3.8": - create_cmd += ["--wheel", "bundle"] - result = cli_run(create_cmd) - assert not (result.creator.purelib / pkg).exists() - for key in {"pip", "setuptools", "wheel"} - {pkg}: - if for_py_version != "3.8" and key == "wheel": - continue - assert (result.creator.purelib / key).exists() - - -@pytest.mark.usefixtures("temp_app_data") -def test_app_data_parallel_ok(tmp_path) -> None: - exceptions = _run_parallel_threads(tmp_path) - assert not exceptions, "\n".join(exceptions) - - -@pytest.mark.usefixtures("temp_app_data") -def test_app_data_parallel_fail(tmp_path: Path, mocker: MockerFixture) -> None: - mocker.patch("virtualenv.seed.embed.via_app_data.pip_install.base.PipInstall.build_image", side_effect=RuntimeError) - exceptions = _run_parallel_threads(tmp_path) - assert len(exceptions) == 2 - for exception in exceptions: - assert exception.startswith("failed to build image pip because:\nTraceback") - assert "RuntimeError" in exception, exception - - -def _run_parallel_threads(tmp_path): - exceptions = [] - - def _run(name) -> None: - try: - cmd = ["--seeder", "app-data", str(tmp_path / name), "--no-setuptools"] - if sys.version_info[:2] == (3, 8): - cmd.append("--no-wheel") - cli_run(cmd) - except Exception as exception: # noqa: BLE001 - as_str = str(exception) - exceptions.append(as_str) - - threads = [Thread(target=_run, args=(f"env{i}",)) for i in range(1, 3)] - for thread in threads: - thread.start() - for thread in threads: - thread.join() - return exceptions diff --git a/tests/unit/seed/embed/test_pip_invoke.py b/tests/unit/seed/embed/test_pip_invoke.py deleted file mode 100644 index 6753fc20a..000000000 --- a/tests/unit/seed/embed/test_pip_invoke.py +++ /dev/null @@ -1,93 +0,0 @@ -from __future__ import annotations - -import itertools -import sys -from shutil import copy2 - -import pytest - -from virtualenv.run import cli_run -from virtualenv.seed.embed.pip_invoke import PipInvoke -from virtualenv.seed.wheels.bundle import load_embed_wheel -from virtualenv.seed.wheels.embed import BUNDLE_FOLDER, BUNDLE_SUPPORT - - -@pytest.mark.slow -@pytest.mark.parametrize("no", ["pip", "setuptools", "wheel", ""]) -def test_base_bootstrap_via_pip_invoke(tmp_path, coverage_env, mocker, current_fastest, no) -> None: # noqa: C901 - extra_search_dir = tmp_path / "extra" - extra_search_dir.mkdir() - for_py_version = f"{sys.version_info.major}.{sys.version_info.minor}" - new = BUNDLE_SUPPORT[for_py_version] - for wheel_filename in BUNDLE_SUPPORT[for_py_version].values(): - copy2(str(BUNDLE_FOLDER / wheel_filename), str(extra_search_dir)) - - def _load_embed_wheel(app_data, distribution, _for_py_version, version): - return load_embed_wheel(app_data, distribution, old_ver, version) - - old_ver = "3.8" - old = BUNDLE_SUPPORT[old_ver] - mocker.patch("virtualenv.seed.wheels.bundle.load_embed_wheel", side_effect=_load_embed_wheel) - - def _execute(cmd, env): - expected = set() - for distribution, with_version in versions.items(): - if distribution == no: - continue - if with_version == "embed" or old[distribution] == new[distribution]: - expected.add(BUNDLE_FOLDER) - else: - expected.add(extra_search_dir) - expected_list = list( - itertools.chain.from_iterable(["--find-links", str(e)] for e in sorted(expected, key=str)), - ) - found = cmd[-len(expected_list) :] if expected_list else [] - assert "--no-index" not in cmd - cmd.append("--no-index") - assert found == expected_list - return original(cmd, env) - - original = PipInvoke._execute # noqa: SLF001 - run = mocker.patch.object(PipInvoke, "_execute", side_effect=_execute) - versions = {"pip": "embed", "setuptools": "bundle"} - if sys.version_info[:2] == (3, 8): - versions["wheel"] = new["wheel"].split("-")[1] - - create_cmd = [ - "--seeder", - "pip", - str(tmp_path / "env"), - "--download", - "--creator", - current_fastest, - "--extra-search-dir", - str(extra_search_dir), - "--app-data", - str(tmp_path / "app-data"), - ] - for dist, version in versions.items(): - create_cmd.extend([f"--{dist}", version]) - if no: - create_cmd.append(f"--no-{no}") - result = cli_run(create_cmd) - coverage_env() - - assert result - assert run.call_count == 1 - - site_package = result.creator.purelib - pip = site_package / "pip" - setuptools = site_package / "setuptools" - wheel = site_package / "wheel" - files_post_first_create = list(site_package.iterdir()) - - if no: - no_file = locals()[no] - assert no not in files_post_first_create - - for key in ("pip", "setuptools", "wheel"): - if key == no: - continue - if sys.version_info[:2] >= (3, 9) and key == "wheel": - continue - assert locals()[key] in files_post_first_create diff --git a/tests/unit/seed/wheels/test_acquire.py b/tests/unit/seed/wheels/test_acquire.py deleted file mode 100644 index 7754a8cb9..000000000 --- a/tests/unit/seed/wheels/test_acquire.py +++ /dev/null @@ -1,173 +0,0 @@ -from __future__ import annotations - -import os -import sys -from datetime import datetime, timezone -from pathlib import Path -from subprocess import CalledProcessError -from typing import TYPE_CHECKING - -import pytest - -from virtualenv.app_data import AppDataDiskFolder -from virtualenv.seed.wheels.acquire import download_wheel, get_wheel, pip_wheel_env_run -from virtualenv.seed.wheels.embed import BUNDLE_FOLDER, get_embed_wheel -from virtualenv.seed.wheels.periodic_update import dump_datetime -from virtualenv.seed.wheels.util import Wheel, discover_wheels - -if TYPE_CHECKING: - from collections.abc import Callable - from unittest.mock import MagicMock - - from pytest_mock import MockerFixture - - -@pytest.fixture(autouse=True) -def _fake_release_date(mocker) -> None: - mocker.patch("virtualenv.seed.wheels.periodic_update.release_date_for_wheel_path", return_value=None) - - -def test_pip_wheel_env_run_could_not_find(session_app_data, mocker) -> None: - mocker.patch("virtualenv.seed.wheels.acquire.from_bundle", return_value=None) - with pytest.raises(RuntimeError, match="could not find the embedded pip"): - pip_wheel_env_run([], session_app_data, os.environ) - - -def test_download_wheel_bad_output(mocker, for_py_version, session_app_data) -> None: - """if the download contains no match for what wheel was downloaded, pick one that matches from target""" - distribution = "setuptools" - p_open = mocker.MagicMock() - mocker.patch("virtualenv.seed.wheels.acquire.Popen", return_value=p_open) - p_open.communicate.return_value = "", "" - p_open.returncode = 0 - - embed = get_embed_wheel(distribution, for_py_version) - as_path = mocker.MagicMock() - available = discover_wheels(BUNDLE_FOLDER, "setuptools", None, for_py_version) - as_path.iterdir.return_value = [i.path for i in available] - - result = download_wheel( - distribution, - f"=={embed.version}", - for_py_version, - [], - session_app_data, - as_path, - os.environ, - ) - assert result.path == embed.path - - -def test_download_fails(mocker, for_py_version, session_app_data) -> None: - p_open = mocker.MagicMock() - mocker.patch("virtualenv.seed.wheels.acquire.Popen", return_value=p_open) - p_open.communicate.return_value = "out", "err" - p_open.returncode = 1 - - as_path = mocker.MagicMock() - with pytest.raises(CalledProcessError) as context: - download_wheel("pip", "==1", for_py_version, [], session_app_data, as_path, os.environ) - exc = context.value - assert exc.output == "out" - assert exc.stderr == "err" - assert exc.returncode == 1 - assert [ - sys.executable, - "-m", - "pip", - "download", - "--progress-bar", - "off", - "--disable-pip-version-check", - "--only-binary=:all:", - "--no-deps", - "--python-version", - for_py_version, - "-d", - str(as_path), - "pip==1", - ] == exc.cmd - - -def test_download_wheel_python_io_encoding(mocker, for_py_version, session_app_data) -> None: - mock_popen = mocker.patch("virtualenv.seed.wheels.acquire.Popen") - mock_popen.return_value.communicate.return_value = "Saved a-b-c.whl", "" - mock_popen.return_value.returncode = 0 - mocker.patch("pathlib.Path.absolute", return_value=Path("a-b-c.whl")) - - download_wheel("pip", "==1", for_py_version, [], session_app_data, "folder", os.environ.copy()) - - env = mock_popen.call_args[1]["env"] - assert env["PYTHONIOENCODING"] == "utf-8" - - -@pytest.fixture -def downloaded_wheel(mocker): - wheel = Wheel.from_path(Path("setuptools-0.0.0-py2.py3-none-any.whl")) - return wheel, mocker.patch("virtualenv.seed.wheels.acquire.download_wheel", return_value=wheel) - - -@pytest.mark.parametrize("version", ["bundle", "0.0.0"]) -def test_get_wheel_download_called(mocker, for_py_version, session_app_data, downloaded_wheel, version) -> None: - distribution = "setuptools" - write = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.write") - wheel = get_wheel(distribution, version, for_py_version, [], True, session_app_data, False, os.environ) - assert wheel is not None - assert wheel.name == downloaded_wheel[0].name - assert downloaded_wheel[1].call_count == 1 - assert write.call_count == 1 - - -@pytest.mark.parametrize("version", ["embed", "pinned"]) -def test_get_wheel_download_not_called(mocker, for_py_version, session_app_data, downloaded_wheel, version) -> None: - distribution = "setuptools" - expected = get_embed_wheel(distribution, for_py_version) - if version == "pinned": - version = expected.version - write = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.write") - wheel = get_wheel(distribution, version, for_py_version, [], True, session_app_data, False, os.environ) - assert wheel is not None - assert wheel.name == expected.name - assert downloaded_wheel[1].call_count == 0 - assert write.call_count == 0 - - -def test_get_wheel_download_cached( - tmp_path: Path, - mocker: MockerFixture, - for_py_version: str, - downloaded_wheel: tuple[Wheel, MagicMock], - time_freeze: Callable[[datetime], None], -) -> None: - time_freeze(datetime.now(tz=timezone.utc)) - from virtualenv.app_data.via_disk_folder import JSONStoreDisk # noqa: PLC0415 - - app_data = AppDataDiskFolder(folder=str(tmp_path)) - expected = downloaded_wheel[0] - write = mocker.spy(JSONStoreDisk, "write") - # 1st call, not cached, download is called - wheel = get_wheel(expected.distribution, expected.version, for_py_version, [], True, app_data, False, os.environ) - assert wheel is not None - assert wheel.name == expected.name - assert downloaded_wheel[1].call_count == 1 - assert write.call_count == 1 - # 2nd call, cached, download is not called - wheel = get_wheel(expected.distribution, expected.version, for_py_version, [], True, app_data, False, os.environ) - assert wheel is not None - assert wheel.name == expected.name - assert downloaded_wheel[1].call_count == 1 - assert write.call_count == 1 - wrote_json = write.call_args[0][1] - assert wrote_json == { - "completed": None, - "periodic": None, - "started": None, - "versions": [ - { - "filename": expected.name, - "release_date": None, - "found_date": dump_datetime(datetime.now(tz=timezone.utc)), - "source": "download", - }, - ], - } diff --git a/tests/unit/seed/wheels/test_acquire_find_wheel.py b/tests/unit/seed/wheels/test_acquire_find_wheel.py deleted file mode 100644 index 71d71525b..000000000 --- a/tests/unit/seed/wheels/test_acquire_find_wheel.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import annotations - -import pytest - -from virtualenv.seed.wheels.acquire import find_compatible_in_house -from virtualenv.seed.wheels.embed import BUNDLE_FOLDER, MAX, get_embed_wheel - - -def test_find_latest_none(for_py_version) -> None: - result = find_compatible_in_house("setuptools", None, for_py_version, BUNDLE_FOLDER) - expected = get_embed_wheel("setuptools", for_py_version) - assert result.path == expected.path - - -def test_find_latest_string(for_py_version) -> None: - result = find_compatible_in_house("setuptools", "", for_py_version, BUNDLE_FOLDER) - expected = get_embed_wheel("setuptools", for_py_version) - assert result.path == expected.path - - -def test_find_exact(for_py_version) -> None: - expected = get_embed_wheel("setuptools", for_py_version) - result = find_compatible_in_house("setuptools", f"=={expected.version}", for_py_version, BUNDLE_FOLDER) - assert result.path == expected.path - - -def test_find_bad_spec() -> None: - with pytest.raises(ValueError, match="bad"): - find_compatible_in_house("setuptools", "bad", MAX, BUNDLE_FOLDER) diff --git a/tests/unit/seed/wheels/test_bundle.py b/tests/unit/seed/wheels/test_bundle.py deleted file mode 100644 index 4cb329569..000000000 --- a/tests/unit/seed/wheels/test_bundle.py +++ /dev/null @@ -1,75 +0,0 @@ -from __future__ import annotations - -import os -from datetime import datetime, timezone -from pathlib import Path - -import pytest - -from virtualenv.app_data import AppDataDiskFolder -from virtualenv.seed.wheels.bundle import from_bundle -from virtualenv.seed.wheels.embed import get_embed_wheel -from virtualenv.seed.wheels.periodic_update import dump_datetime -from virtualenv.seed.wheels.util import Version, Wheel - - -@pytest.fixture(scope="module") -def next_pip_wheel(for_py_version): - wheel = get_embed_wheel("pip", for_py_version) - new_version = list(wheel.version_tuple) - new_version[-1] += 1 - new_name = wheel.name.replace(wheel.version, ".".join(str(i) for i in new_version)) - return Wheel.from_path(Path(new_name)) - - -@pytest.fixture(scope="module") -def app_data(tmp_path_factory, for_py_version, next_pip_wheel): - temp_folder = tmp_path_factory.mktemp("module-app-data") - now = dump_datetime(datetime.now(tz=timezone.utc)) - app_data_ = AppDataDiskFolder(str(temp_folder)) - app_data_.embed_update_log("pip", for_py_version).write( - { - "completed": now, - "periodic": True, - "started": now, - "versions": [ - { - "filename": next_pip_wheel.name, - "found_date": "2000-01-01T00:00:00.000000Z", - "release_date": "2000-01-01T00:00:00.000000Z", - "source": "periodic", - }, - ], - }, - ) - return app_data_ - - -def test_version_embed(app_data, for_py_version) -> None: - wheel = from_bundle("pip", Version.embed, for_py_version, [], app_data, False, os.environ) - assert wheel is not None - assert wheel.name == get_embed_wheel("pip", for_py_version).name - - -def test_version_bundle(app_data, for_py_version, next_pip_wheel) -> None: - wheel = from_bundle("pip", Version.bundle, for_py_version, [], app_data, False, os.environ) - assert wheel is not None - assert wheel.name == next_pip_wheel.name - - -def test_version_pinned_not_found(app_data, for_py_version) -> None: - wheel = from_bundle("pip", "0.0.0", for_py_version, [], app_data, False, os.environ) - assert wheel is None - - -def test_version_pinned_is_embed(app_data, for_py_version) -> None: - expected_wheel = get_embed_wheel("pip", for_py_version) - wheel = from_bundle("pip", expected_wheel.version, for_py_version, [], app_data, False, os.environ) - assert wheel is not None - assert wheel.name == expected_wheel.name - - -def test_version_pinned_in_app_data(app_data, for_py_version, next_pip_wheel) -> None: - wheel = from_bundle("pip", next_pip_wheel.version, for_py_version, [], app_data, False, os.environ) - assert wheel is not None - assert wheel.name == next_pip_wheel.name diff --git a/tests/unit/seed/wheels/test_periodic_update.py b/tests/unit/seed/wheels/test_periodic_update.py deleted file mode 100644 index e683d6cf3..000000000 --- a/tests/unit/seed/wheels/test_periodic_update.py +++ /dev/null @@ -1,706 +0,0 @@ -from __future__ import annotations - -import json -import os -import subprocess -import sys -from collections import defaultdict -from contextlib import contextmanager -from datetime import datetime, timedelta, timezone -from io import StringIO -from itertools import zip_longest -from pathlib import Path -from textwrap import dedent -from urllib.error import URLError - -import pytest - -from virtualenv import cli_run -from virtualenv.app_data import AppDataDiskFolder -from virtualenv.seed.wheels import Wheel -from virtualenv.seed.wheels.embed import BUNDLE_SUPPORT, get_embed_wheel -from virtualenv.seed.wheels.periodic_update import ( - NewVersion, - UpdateLog, - do_update, - dump_datetime, - load_datetime, - manual_upgrade, - periodic_update, - release_date_for_wheel_path, - trigger_update, -) -from virtualenv.util.subprocess import CREATE_NO_WINDOW - - -@pytest.fixture(autouse=True) -def _clear_pypi_info_cache() -> None: - from virtualenv.seed.wheels.periodic_update import _PYPI_CACHE # noqa: PLC0415 - - _PYPI_CACHE.clear() - - -def test_manual_upgrade(session_app_data, caplog, mocker, for_py_version) -> None: - wheel = get_embed_wheel("pip", for_py_version) - new_version = NewVersion( - wheel.path, - datetime.now(tz=timezone.utc), - datetime.now(tz=timezone.utc) - timedelta(days=20), - "manual", - ) - - def _do_update(distribution, **_kwargs): - if distribution == "pip": - return [new_version] - return [] - - do_update_mock = mocker.patch("virtualenv.seed.wheels.periodic_update.do_update", side_effect=_do_update) - manual_upgrade(session_app_data, os.environ) - - assert "upgrade pip" in caplog.text - assert "upgraded pip" in caplog.text - assert " no new versions found" in caplog.text - assert " new entries found:\n" in caplog.text - assert "\tNewVersion(" in caplog.text - packages = defaultdict(list) - for args in do_update_mock.call_args_list: - packages[args[1]["distribution"]].append(args[1]["for_py_version"]) - packages = {key: sorted(value) for key, value in packages.items()} - versions = sorted(BUNDLE_SUPPORT.keys()) - expected = {"setuptools": versions, "wheel": ["3.8"], "pip": versions} - assert packages == expected - - -@pytest.mark.usefixtures("session_app_data") -def test_pick_periodic_update(tmp_path, mocker, for_py_version) -> None: - embed, current = get_embed_wheel("setuptools", "3.6"), get_embed_wheel("setuptools", for_py_version) - mocker.patch("virtualenv.seed.wheels.bundle.load_embed_wheel", return_value=embed) - completed = datetime.now(tz=timezone.utc) - timedelta(days=29) - u_log = UpdateLog( - started=datetime.now(tz=timezone.utc) - timedelta(days=30), - completed=completed, - versions=[NewVersion(filename=current.path, found_date=completed, release_date=completed, source="periodic")], - periodic=True, - ) - read_dict = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.read", return_value=u_log.to_dict()) - - result = cli_run( - [ - str(tmp_path), - "--activators", - "", - "--no-periodic-update", - "--no-wheel", - "--no-pip", - "--setuptools", - "bundle", - ], - ) - - assert read_dict.call_count == 1 - installed = [i.name for i in result.creator.purelib.iterdir() if i.suffix == ".dist-info"] - assert f"setuptools-{current.version}.dist-info" in installed - - -def test_periodic_update_stops_at_current(mocker, session_app_data, for_py_version) -> None: - current = get_embed_wheel("setuptools", for_py_version) - - now, completed = datetime.now(tz=timezone.utc), datetime.now(tz=timezone.utc) - timedelta(days=29) - u_log = UpdateLog( - started=completed, - completed=completed, - versions=[ - NewVersion(wheel_path(current, (1,)), completed, now - timedelta(days=1), "periodic"), - NewVersion(current.path, completed, now - timedelta(days=2), "periodic"), - NewVersion(wheel_path(current, (-1,)), completed, now - timedelta(days=30), "periodic"), - ], - periodic=True, - ) - mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.read", return_value=u_log.to_dict()) - - result = periodic_update("setuptools", None, for_py_version, current, [], session_app_data, False, os.environ) - assert result.path == current.path - - -def test_periodic_update_latest_per_patch(mocker, session_app_data, for_py_version) -> None: - current = get_embed_wheel("setuptools", for_py_version) - expected_path = wheel_path(current, (0, 1, 2)) - now = datetime.now(tz=timezone.utc) - completed = now - timedelta(hours=2) - u_log = UpdateLog( - started=completed, - completed=completed, - periodic=True, - versions=[ - NewVersion(expected_path, completed, now - timedelta(days=1), "periodic"), - NewVersion(wheel_path(current, (0, 1, 1)), completed, now - timedelta(days=30), "periodic"), - NewVersion(str(current.path), completed, now - timedelta(days=31), "periodic"), - ], - ) - mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.read", return_value=u_log.to_dict()) - - result = periodic_update("setuptools", None, for_py_version, current, [], session_app_data, False, os.environ) - assert str(result.path) == expected_path - - -def test_periodic_update_latest_per_patch_prev_is_manual(mocker, session_app_data, for_py_version) -> None: - current = get_embed_wheel("setuptools", for_py_version) - expected_path = wheel_path(current, (0, 1, 2)) - now = datetime.now(tz=timezone.utc) - completed = now - timedelta(hours=2) - u_log = UpdateLog( - started=completed, - completed=completed, - periodic=True, - versions=[ - NewVersion(expected_path, completed, completed, "periodic"), - NewVersion(wheel_path(current, (0, 1, 1)), completed, now - timedelta(days=10), "manual"), - NewVersion(wheel_path(current, (0, 1, 0)), completed, now - timedelta(days=11), "periodic"), - NewVersion(str(current.path), completed, now - timedelta(days=12), "manual"), - ], - ) - mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.read", return_value=u_log.to_dict()) - - result = periodic_update("setuptools", None, for_py_version, current, [], session_app_data, False, os.environ) - assert str(result.path) == expected_path - - -def test_manual_update_honored(mocker, session_app_data, for_py_version) -> None: - current = get_embed_wheel("setuptools", for_py_version) - expected_path = wheel_path(current, (0, 1, 1)) - now = datetime.now(tz=timezone.utc) - completed = now - u_log = UpdateLog( - started=completed, - completed=completed, - periodic=True, - versions=[ - NewVersion(wheel_path(current, (0, 1, 2)), completed, completed, "periodic"), - NewVersion(expected_path, completed, now - timedelta(days=10), "manual"), - NewVersion(wheel_path(current, (0, 1, 0)), completed, now - timedelta(days=11), "periodic"), - NewVersion(str(current.path), completed, now - timedelta(days=12), "manual"), - ], - ) - mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.read", return_value=u_log.to_dict()) - - result = periodic_update("setuptools", None, for_py_version, current, [], session_app_data, False, os.environ) - assert str(result.path) == expected_path - - -def wheel_path(wheel, of, pre_release=""): - new_version = ".".join(str(i) for i in (tuple(sum(x) for x in zip_longest(wheel.version_tuple, of, fillvalue=0)))) - new_name = wheel.name.replace(wheel.version, new_version + pre_release) - return str(wheel.path.parent / new_name) - - -_UP_NOW = datetime.now(tz=timezone.utc) -_UPDATE_SKIP = { - "started_just_now_no_complete": UpdateLog(started=_UP_NOW, completed=None, versions=[], periodic=True), - "started_1_hour_no_complete": UpdateLog( - started=_UP_NOW - timedelta(hours=1), - completed=None, - versions=[], - periodic=True, - ), - "completed_under_two_weeks": UpdateLog( - started=None, - completed=_UP_NOW - timedelta(days=14), - versions=[], - periodic=True, - ), - "started_just_now_completed_two_weeks": UpdateLog( - started=_UP_NOW, - completed=_UP_NOW - timedelta(days=14, seconds=1), - versions=[], - periodic=True, - ), - "started_1_hour_completed_two_weeks": UpdateLog( - started=_UP_NOW - timedelta(hours=1), - completed=_UP_NOW - timedelta(days=14, seconds=1), - versions=[], - periodic=True, - ), -} - - -@pytest.mark.parametrize("u_log", list(_UPDATE_SKIP.values()), ids=list(_UPDATE_SKIP.keys())) -def test_periodic_update_skip(u_log, mocker, for_py_version, session_app_data, time_freeze) -> None: - time_freeze(_UP_NOW) - mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.read", return_value=u_log.to_dict()) - mocker.patch("virtualenv.seed.wheels.periodic_update.trigger_update", side_effect=RuntimeError) - - result = periodic_update("setuptools", None, for_py_version, None, [], session_app_data, os.environ, True) - assert result is None - - -_UPDATE_YES = { - "never_started": UpdateLog(started=None, completed=None, versions=[], periodic=False), - "started_1_hour": UpdateLog( - started=_UP_NOW - timedelta(hours=1, microseconds=1), - completed=None, - versions=[], - periodic=False, - ), - "completed_two_week": UpdateLog( - started=_UP_NOW - timedelta(days=14, microseconds=2), - completed=_UP_NOW - timedelta(days=14, microseconds=1), - versions=[], - periodic=False, - ), -} - - -@pytest.mark.parametrize("u_log", list(_UPDATE_YES.values()), ids=list(_UPDATE_YES.keys())) -def test_periodic_update_trigger(u_log, mocker, for_py_version, session_app_data, time_freeze) -> None: - time_freeze(_UP_NOW) - mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.read", return_value=u_log.to_dict()) - write = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.write") - trigger_update_ = mocker.patch("virtualenv.seed.wheels.periodic_update.trigger_update") - - result = periodic_update("setuptools", None, for_py_version, None, [], session_app_data, os.environ, True) - - assert result is None - assert trigger_update_.call_count - assert write.call_count == 1 - wrote_json = write.call_args[0][0] - assert wrote_json["periodic"] is True - assert load_datetime(wrote_json["started"]) == _UP_NOW - - -def test_trigger_update_no_debug(for_py_version, session_app_data, tmp_path, mocker, monkeypatch) -> None: - monkeypatch.delenv("_VIRTUALENV_PERIODIC_UPDATE_INLINE", raising=False) - current = get_embed_wheel("setuptools", for_py_version) - process = mocker.MagicMock() - process.pid = 123 - process.communicate.return_value = None, None - Popen = mocker.patch("virtualenv.seed.wheels.periodic_update.Popen", return_value=process) # noqa: N806 - - trigger_update( - "setuptools", - for_py_version, - current, - [tmp_path / "a", tmp_path / "b"], - session_app_data, - os.environ, - True, - ) - - assert Popen.call_count == 1 - args, kwargs = Popen.call_args - cmd = ( - dedent( - """ - from virtualenv.report import setup_report, MAX_LEVEL - from virtualenv.seed.wheels.periodic_update import do_update - setup_report(MAX_LEVEL, show_pid=True) - do_update({!r}, {!r}, {!r}, {!r}, {!r}, {!r}) - """, - ) - .strip() - .format( - "setuptools", - for_py_version, - str(current.path), - str(session_app_data), - [str(tmp_path / "a"), str(tmp_path / "b")], - True, - ) - ) - - assert args == ([sys.executable, "-c", cmd],) - expected = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL} - if sys.platform == "win32": - expected["creationflags"] = CREATE_NO_WINDOW - assert kwargs == expected - assert process.communicate.call_count == 0 - - -def test_trigger_update_debug(for_py_version, session_app_data, tmp_path, mocker, monkeypatch) -> None: - monkeypatch.setenv("_VIRTUALENV_PERIODIC_UPDATE_INLINE", "1") - current = get_embed_wheel("pip", for_py_version) - - process = mocker.MagicMock() - process.pid = 123 - process.communicate.return_value = None, None - Popen = mocker.patch("virtualenv.seed.wheels.periodic_update.Popen", return_value=process) # noqa: N806 - - trigger_update( - "pip", - for_py_version, - current, - [tmp_path / "a", tmp_path / "b"], - session_app_data, - os.environ, - False, - ) - - assert Popen.call_count == 1 - args, kwargs = Popen.call_args - cmd = ( - dedent( - """ - from virtualenv.report import setup_report, MAX_LEVEL - from virtualenv.seed.wheels.periodic_update import do_update - setup_report(MAX_LEVEL, show_pid=True) - do_update({!r}, {!r}, {!r}, {!r}, {!r}, {!r}) - """, - ) - .strip() - .format( - "pip", - for_py_version, - str(current.path), - str(session_app_data), - [str(tmp_path / "a"), str(tmp_path / "b")], - False, - ) - ) - assert args == ([sys.executable, "-c", cmd],) - expected = {"stdout": None, "stderr": None} - assert kwargs == expected - assert process.communicate.call_count == 1 - - -def test_do_update_first(tmp_path, mocker, time_freeze) -> None: - time_freeze(_UP_NOW) - wheel = get_embed_wheel("pip", "3.9") - app_data_outer = AppDataDiskFolder(str(tmp_path / "app")) - extra = tmp_path / "extra" - extra.mkdir() - - pip_version_remote = [ - (wheel_path(wheel, (1, 0, 0)), None), - (wheel_path(wheel, (0, 1, 0)), _UP_NOW - timedelta(days=1)), - (wheel_path(wheel, (0, 0, 1)), _UP_NOW - timedelta(days=2)), - (wheel.path, _UP_NOW - timedelta(days=3)), - (wheel_path(wheel, (-1, 0, 0)), _UP_NOW - timedelta(days=30)), - ] - download_wheels = (Wheel(Path(i[0])) for i in pip_version_remote) - - def _download_wheel( # noqa: PLR0913 - distribution, - version_spec, # noqa: ARG001 - for_py_version, - search_dirs, - app_data, - to_folder, - env, # noqa: ARG001 - ): - assert distribution == "pip" - assert for_py_version == "3.9" - assert [str(i) for i in search_dirs] == [str(extra)] - assert isinstance(app_data, AppDataDiskFolder) - assert to_folder == app_data_outer.house - return next(download_wheels) - - download_wheel = mocker.patch("virtualenv.seed.wheels.acquire.download_wheel", side_effect=_download_wheel) - releases = { - Wheel(Path(wheel)).version: [ - {"upload_time": datetime.strftime(release_date, "%Y-%m-%dT%H:%M:%S") if release_date is not None else None}, - ] - for wheel, release_date in pip_version_remote - } - pypi_release = json.dumps({"releases": releases}) - - @contextmanager - def _release(of, context): - assert of == "https://pypi.org/pypi/pip/json" - assert context is None - yield StringIO(pypi_release) - - url_o = mocker.patch("virtualenv.seed.wheels.periodic_update.urlopen", side_effect=_release) - - last_update = _UP_NOW - timedelta(days=14) - u_log = UpdateLog(started=last_update, completed=last_update, versions=[], periodic=True) - read_dict = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.read", return_value=u_log.to_dict()) - write = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.write") - copy = mocker.patch("virtualenv.seed.wheels.periodic_update.copy2") - - versions = do_update("pip", "3.9", str(pip_version_remote[-1][0]), str(app_data_outer), [str(extra)], True) - - assert download_wheel.call_count == len(pip_version_remote) - assert url_o.call_count == 1 - assert copy.call_count == 1 - - expected = [ - NewVersion(Path(wheel).name, _UP_NOW, None if release is None else release.replace(microsecond=0), "periodic") - for wheel, release in pip_version_remote - ] - assert versions == expected - - assert read_dict.call_count == 1 - assert write.call_count == 1 - wrote_json = write.call_args[0][0] - assert wrote_json == { - "started": dump_datetime(last_update), - "completed": dump_datetime(_UP_NOW), - "periodic": True, - "versions": [e.to_dict() for e in expected], - } - - -def test_do_update_skip_already_done(tmp_path, mocker, time_freeze) -> None: - time_freeze(_UP_NOW + timedelta(hours=1)) - wheel = get_embed_wheel("pip", "3.9") - app_data_outer = AppDataDiskFolder(str(tmp_path / "app")) - extra = tmp_path / "extra" - extra.mkdir() - - def _download_wheel(**_kwargs): - return wheel.path - - download_wheel = mocker.patch("virtualenv.seed.wheels.acquire.download_wheel", side_effect=_download_wheel) - url_o = mocker.patch("virtualenv.seed.wheels.periodic_update.urlopen", side_effect=RuntimeError) - - released = _UP_NOW - timedelta(days=30) - u_log = UpdateLog( - started=_UP_NOW - timedelta(days=31), - completed=released, - versions=[NewVersion(filename=wheel.path.name, found_date=released, release_date=released, source="periodic")], - periodic=True, - ) - read_dict = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.read", return_value=u_log.to_dict()) - write = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.write") - - versions = do_update("pip", "3.9", str(wheel.path), str(app_data_outer), [str(extra)], False) - - assert download_wheel.call_count == 1 - assert read_dict.call_count == 1 - assert not url_o.call_count - assert versions == [] - - assert write.call_count == 1 - wrote_json = write.call_args[0][0] - assert wrote_json == { - "started": dump_datetime(_UP_NOW + timedelta(hours=1)), - "completed": dump_datetime(_UP_NOW + timedelta(hours=1)), - "periodic": False, - "versions": [ - { - "filename": wheel.path.name, - "release_date": dump_datetime(released), - "found_date": dump_datetime(released), - "source": "manual", # changed from "periodic" to "manual" - }, - ], - } - - -def test_new_version_eq() -> None: - now = datetime.now(tz=timezone.utc) - value = NewVersion("a", now, now, "periodic") - assert value == NewVersion("a", now, now, "periodic") - - -def test_new_version_ne() -> None: - assert NewVersion("a", datetime.now(tz=timezone.utc), datetime.now(tz=timezone.utc), "periodic") != NewVersion( - "a", - datetime.now(tz=timezone.utc), - datetime.now(tz=timezone.utc) + timedelta(hours=1), - "manual", - ) - - -def test_get_release_unsecure(mocker, caplog) -> None: - @contextmanager - def _release(of, context): - assert of == "https://pypi.org/pypi/pip/json" - if context is None: - msg = "insecure" - raise URLError(msg) - assert context - yield StringIO(json.dumps({"releases": {"20.1": [{"upload_time": "2020-12-22T12:12:12"}]}})) - - url_o = mocker.patch("virtualenv.seed.wheels.periodic_update.urlopen", side_effect=_release) - - result = release_date_for_wheel_path(Path("pip-20.1.whl")) - - assert result == datetime(year=2020, month=12, day=22, hour=12, minute=12, second=12, tzinfo=timezone.utc) - assert url_o.call_count == 2 - assert "insecure" in caplog.text - assert " failed " in caplog.text - - -def test_get_release_fails(mocker, caplog) -> None: - exc = RuntimeError("oh no") - url_o = mocker.patch("virtualenv.seed.wheels.periodic_update.urlopen", side_effect=exc) - - result = release_date_for_wheel_path(Path("pip-20.1.whl")) - - assert result is None - assert url_o.call_count == 1 - assert repr(exc) in caplog.text - - -def mock_download(mocker, pip_version_remote): - def download(): - index = 0 - while True: - path = pip_version_remote[index] - index += 1 - yield Wheel(Path(path)) - - do = download() - return mocker.patch( - "virtualenv.seed.wheels.acquire.download_wheel", - side_effect=lambda *_a, **_k: next(do), - ) - - -def test_download_stop_with_embed(tmp_path, mocker, time_freeze) -> None: - time_freeze(_UP_NOW) - wheel = get_embed_wheel("pip", "3.9") - app_data_outer = AppDataDiskFolder(str(tmp_path / "app")) - pip_version_remote = [wheel_path(wheel, (0, 0, 2)), wheel_path(wheel, (0, 0, 1)), wheel_path(wheel, (-1, 0, 0))] - - download_wheel = mock_download(mocker, pip_version_remote) - url_o = mocker.patch("virtualenv.seed.wheels.periodic_update.urlopen", side_effect=URLError("unavailable")) - - last_update = _UP_NOW - timedelta(days=14) - u_log = UpdateLog(started=last_update, completed=last_update, versions=[], periodic=True) - read_dict = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.read", return_value=u_log.to_dict()) - write = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.write") - - do_update("pip", "3.9", str(wheel.path), str(app_data_outer), [], True) - - assert download_wheel.call_count == 3 - assert url_o.call_count == 2 - - assert read_dict.call_count == 1 - assert write.call_count == 1 - - -def test_download_manual_stop_after_one_download(tmp_path, mocker, time_freeze) -> None: - time_freeze(_UP_NOW) - wheel = get_embed_wheel("pip", "3.9") - app_data_outer = AppDataDiskFolder(str(tmp_path / "app")) - pip_version_remote = [wheel_path(wheel, (0, 1, 1))] - - download_wheel = mock_download(mocker, pip_version_remote) - url_o = mocker.patch("virtualenv.seed.wheels.periodic_update.urlopen", side_effect=URLError("unavailable")) - - last_update = _UP_NOW - timedelta(days=14) - u_log = UpdateLog(started=last_update, completed=last_update, versions=[], periodic=True) - read_dict = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.read", return_value=u_log.to_dict()) - write = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.write") - - do_update("pip", "3.9", str(wheel.path), str(app_data_outer), [], False) - - assert download_wheel.call_count == 1 - assert url_o.call_count == 2 - assert read_dict.call_count == 1 - assert write.call_count == 1 - - -def test_download_manual_ignores_pre_release(tmp_path, mocker, time_freeze) -> None: - time_freeze(_UP_NOW) - wheel = get_embed_wheel("pip", "3.9") - app_data_outer = AppDataDiskFolder(str(tmp_path / "app")) - pip_version_remote = [wheel_path(wheel, (0, 0, 1))] - pip_version_pre = NewVersion(Path(wheel_path(wheel, (0, 1, 0), "b1")).name, _UP_NOW, None, "downloaded") - - download_wheel = mock_download(mocker, pip_version_remote) - url_o = mocker.patch("virtualenv.seed.wheels.periodic_update.urlopen", side_effect=URLError("unavailable")) - - last_update = _UP_NOW - timedelta(days=14) - u_log = UpdateLog(started=last_update, completed=last_update, versions=[pip_version_pre], periodic=True) - read_dict = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.read", return_value=u_log.to_dict()) - write = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.write") - - do_update("pip", "3.9", str(wheel.path), str(app_data_outer), [], False) - - assert download_wheel.call_count == 1 - assert url_o.call_count == 2 - assert read_dict.call_count == 1 - assert write.call_count == 1 - wrote_json = write.call_args[0][0] - assert wrote_json["versions"] == [ - { - "filename": Path(pip_version_remote[0]).name, - "release_date": None, - "found_date": dump_datetime(_UP_NOW), - "source": "manual", - }, - pip_version_pre.to_dict(), - ] - - -def test_download_periodic_stop_at_first_usable(tmp_path, mocker, time_freeze) -> None: - time_freeze(_UP_NOW) - wheel = get_embed_wheel("pip", "3.9") - app_data_outer = AppDataDiskFolder(str(tmp_path / "app")) - pip_version_remote = [wheel_path(wheel, (0, 1, 1)), wheel_path(wheel, (0, 1, 0))] - rel_date_remote = [_UP_NOW - timedelta(days=1), _UP_NOW - timedelta(days=30)] - - download_wheel = mock_download(mocker, pip_version_remote) - - rel_date_gen = iter(rel_date_remote) - release_date = mocker.patch( - "virtualenv.seed.wheels.periodic_update.release_date_for_wheel_path", - side_effect=lambda *_a, **_k: next(rel_date_gen), - ) - - last_update = _UP_NOW - timedelta(days=14) - u_log = UpdateLog(started=last_update, completed=last_update, versions=[], periodic=True) - read_dict = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.read", return_value=u_log.to_dict()) - write = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.write") - - do_update("pip", "3.9", str(wheel.path), str(app_data_outer), [], True) - - assert download_wheel.call_count == 2 - assert release_date.call_count == 2 - assert read_dict.call_count == 1 - assert write.call_count == 1 - - -def test_download_periodic_stop_at_first_usable_with_previous_minor(tmp_path, mocker, time_freeze) -> None: - time_freeze(_UP_NOW) - wheel = get_embed_wheel("pip", "3.9") - app_data_outer = AppDataDiskFolder(str(tmp_path / "app")) - pip_version_remote = [wheel_path(wheel, (0, 1, 1)), wheel_path(wheel, (0, 1, 0)), wheel_path(wheel, (0, -1, 0))] - rel_date_remote = [_UP_NOW - timedelta(days=1), _UP_NOW - timedelta(days=30), _UP_NOW - timedelta(days=40)] - downloaded_versions = [ - NewVersion(Path(pip_version_remote[2]).name, rel_date_remote[2], None, "download"), - NewVersion(Path(pip_version_remote[0]).name, rel_date_remote[0], None, "download"), - ] - - download_wheel = mock_download(mocker, pip_version_remote) - - rel_date_gen = iter(rel_date_remote) - release_date = mocker.patch( - "virtualenv.seed.wheels.periodic_update.release_date_for_wheel_path", - side_effect=lambda *_a, **_k: next(rel_date_gen), - ) - - last_update = _UP_NOW - timedelta(days=14) - u_log = UpdateLog( - started=last_update, - completed=last_update, - versions=downloaded_versions, - periodic=True, - ) - read_dict = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.read", return_value=u_log.to_dict()) - write = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.write") - - do_update("pip", "3.9", str(wheel.path), str(app_data_outer), [], True) - - assert download_wheel.call_count == 2 - assert release_date.call_count == 2 - assert read_dict.call_count == 1 - assert write.call_count == 1 - wrote_json = write.call_args[0][0] - assert wrote_json["versions"] == [ - { - "filename": Path(pip_version_remote[0]).name, - "release_date": dump_datetime(rel_date_remote[0]), - "found_date": dump_datetime(_UP_NOW), - "source": "periodic", - }, - { - "filename": Path(pip_version_remote[1]).name, - "release_date": dump_datetime(rel_date_remote[1]), - "found_date": dump_datetime(_UP_NOW), - "source": "periodic", - }, - downloaded_versions[0].to_dict(), - ] diff --git a/tests/unit/seed/wheels/test_wheels_util.py b/tests/unit/seed/wheels/test_wheels_util.py deleted file mode 100644 index f177a7d21..000000000 --- a/tests/unit/seed/wheels/test_wheels_util.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import annotations - -import pytest - -from virtualenv.seed.wheels.embed import MAX, get_embed_wheel -from virtualenv.seed.wheels.util import Wheel - - -def test_wheel_support_no_python_requires(mocker) -> None: - wheel = get_embed_wheel("setuptools", for_py_version=None) - zip_mock = mocker.MagicMock() - mocker.patch("virtualenv.seed.wheels.util.ZipFile", new=zip_mock) - zip_mock.return_value.__enter__.return_value.read = lambda _name: b"" - - supports = wheel.support_py("3.8") - assert supports is True - - -def test_bad_as_version_tuple() -> None: - with pytest.raises(ValueError, match="bad"): - Wheel.as_version_tuple("bad") - - -def test_wheel_not_support() -> None: - wheel = get_embed_wheel("setuptools", MAX) - assert wheel.support_py("3.3") is False - - -def test_wheel_repr() -> None: - wheel = get_embed_wheel("setuptools", MAX) - assert str(wheel.path) in repr(wheel) - - -def test_unknown_distribution() -> None: - wheel = get_embed_wheel("unknown", MAX) - assert wheel is None diff --git a/tests/unit/test_file_limit.py b/tests/unit/test_file_limit.py deleted file mode 100644 index cde09d0a3..000000000 --- a/tests/unit/test_file_limit.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import annotations - -import errno -import os -import sys - -import pytest - -from virtualenv.info import IMPLEMENTATION -from virtualenv.run import cli_run - - -@pytest.mark.skipif(sys.platform == "win32", reason="resource module not available on Windows") -def test_too_many_open_files(tmp_path) -> None: - """Test that we get a specific error when we have too many open files.""" - import resource # noqa: PLC0415 - - soft_limit, hard_limit = resource.getrlimit(resource.RLIMIT_NOFILE) - - try: - resource.setrlimit(resource.RLIMIT_NOFILE, (32, hard_limit)) - except ValueError: - pytest.skip("could not lower the soft limit for open files") - except AttributeError as exc: # pypy, graalpy - if "module 'resource' has no attribute 'setrlimit'" in str(exc): - pytest.skip(f"{IMPLEMENTATION} does not support resource.setrlimit") - - fds = [] - try: - # JIT implementations may use more file descriptors up front, so we can run out early - try: - fds.extend(os.open(os.devnull, os.O_RDONLY) for _ in range(20)) - except OSError as jit_exc: # pypy, graalpy - assert jit_exc.errno == errno.EMFILE # noqa: PT017 - - expected_exceptions = (SystemExit, OSError, RuntimeError) - with pytest.raises(expected_exceptions) as excinfo: - cli_run([str(tmp_path / "venv")]) - - exc = excinfo.value - if isinstance(exc, SystemExit): - assert exc.code != 0 - elif isinstance(exc, OSError): - assert exc.errno == errno.EMFILE - else: - # RuntimeError wrapper path: don't assert on libc-specific strerror text. - msg = str(exc) - assert ("code 24" in msg) or ("errno 24" in msg) or ("EMFILE" in msg) - - finally: - for fd in fds: - os.close(fd) - resource.setrlimit(resource.RLIMIT_NOFILE, (soft_limit, hard_limit)) diff --git a/tests/unit/test_run.py b/tests/unit/test_run.py deleted file mode 100644 index f223ee1b8..000000000 --- a/tests/unit/test_run.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import annotations - -import logging - -import pytest - -from virtualenv import __version__ -from virtualenv.run import cli_run, session_via_cli - - -def test_help(capsys) -> None: - with pytest.raises(SystemExit) as context: - cli_run(args=["-h", "-vvv"]) - assert context.value.code == 0 - - out, err = capsys.readouterr() - assert not err - assert out - - -def test_version(capsys) -> None: - with pytest.raises(SystemExit) as context: - cli_run(args=["--version"]) - assert context.value.code == 0 - - content, err = capsys.readouterr() - assert not err - - assert __version__ in content - import virtualenv # noqa: PLC0415 - - assert virtualenv.__file__ in content - - -@pytest.mark.parametrize("on", [True, False]) -def test_logging_setup(caplog, on) -> None: - caplog.set_level(logging.DEBUG) - session_via_cli(["env"], setup_logging=on) - # DEBUG only level output is generated during this phase, default output is WARN, so if on no records should be - if on: - assert not caplog.records - else: - assert caplog.records - - -def test_invalid_discovery_method_via_env(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("VIRTUALENV_DISCOVERY", "pyenv") - with pytest.raises(RuntimeError, match=r"discovery 'pyenv' is not available") as exc_info: - session_via_cli(["env"]) - - error_message = str(exc_info.value) - assert "discovery 'pyenv' is not available" in error_message - assert "Available discovery methods:" in error_message - assert "builtin" in error_message - assert "Is the plugin installed?" in error_message diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py deleted file mode 100644 index 2cdc03838..000000000 --- a/tests/unit/test_util.py +++ /dev/null @@ -1,150 +0,0 @@ -from __future__ import annotations - -import concurrent.futures -import os -import traceback -from typing import TYPE_CHECKING - -import pytest - -from virtualenv.app_data import _cache_dir_with_migration, _default_app_data_dir -from virtualenv.util.lock import ReentrantFileLock -from virtualenv.util.subprocess import run_cmd - -if TYPE_CHECKING: - from pathlib import Path - - -def test_run_fail(tmp_path) -> None: - code, out, err = run_cmd([str(tmp_path)]) - assert err - assert not out - assert code - - -def test_reentrant_file_lock_is_thread_safe(tmp_path) -> None: - lock = ReentrantFileLock(tmp_path) - target_file = tmp_path / "target" - target_file.touch() - - def recreate_target_file() -> None: - with lock.lock_for_key("target"): - target_file.unlink() - target_file.touch() - - with concurrent.futures.ThreadPoolExecutor() as executor: - tasks = [executor.submit(recreate_target_file) for _ in range(4)] - concurrent.futures.wait(tasks) - for task in tasks: - try: - task.result() - except Exception: # noqa: BLE001, PERF203 - pytest.fail(traceback.format_exc()) - - -class TestDefaultAppDataDir: - def test_override_env_var(self, tmp_path: Path) -> None: - custom = str(tmp_path / "custom") - env = {"VIRTUALENV_OVERRIDE_APP_DATA": custom} - assert _default_app_data_dir(env) == custom - - def test_no_override_returns_cache_dir(self, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.delenv("VIRTUALENV_OVERRIDE_APP_DATA", raising=False) - result = _default_app_data_dir(os.environ) - assert result - - -class TestCacheDirMigration: - def test_migrate_old_to_new(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - old_dir = str(tmp_path / "old-data") - new_dir = str(tmp_path / "new-cache") - os.makedirs(old_dir) - (tmp_path / "old-data" / "test.txt").write_text("hello") - - monkeypatch.setattr("virtualenv.app_data.user_cache_dir", lambda **_kw: new_dir) - monkeypatch.setattr("virtualenv.app_data.user_data_dir", lambda **_kw: old_dir) - - result = _cache_dir_with_migration() - assert result == new_dir - assert os.path.isdir(new_dir) - assert not os.path.isdir(old_dir) - assert (tmp_path / "new-cache" / "test.txt").read_text() == "hello" - - def test_no_migration_when_old_missing(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - old_dir = str(tmp_path / "old-data") - new_dir = str(tmp_path / "new-cache") - - monkeypatch.setattr("virtualenv.app_data.user_cache_dir", lambda **_kw: new_dir) - monkeypatch.setattr("virtualenv.app_data.user_data_dir", lambda **_kw: old_dir) - - result = _cache_dir_with_migration() - assert result == new_dir - assert not os.path.isdir(old_dir) - - def test_no_migration_when_new_exists(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - old_dir = str(tmp_path / "old-data") - new_dir = str(tmp_path / "new-cache") - os.makedirs(old_dir) - os.makedirs(new_dir) - (tmp_path / "old-data" / "old.txt").write_text("old") - - monkeypatch.setattr("virtualenv.app_data.user_cache_dir", lambda **_kw: new_dir) - monkeypatch.setattr("virtualenv.app_data.user_data_dir", lambda **_kw: old_dir) - - result = _cache_dir_with_migration() - assert result == new_dir - assert os.path.isdir(old_dir) - - def test_same_dir_returns_immediately(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - same_dir = str(tmp_path / "same") - monkeypatch.setattr("virtualenv.app_data.user_cache_dir", lambda **_kw: same_dir) - monkeypatch.setattr("virtualenv.app_data.user_data_dir", lambda **_kw: same_dir) - - result = _cache_dir_with_migration() - assert result == same_dir - - def test_fallback_on_migration_error(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - old_dir = str(tmp_path / "old-data") - new_dir = str(tmp_path / "new-cache") - os.makedirs(old_dir) - - monkeypatch.setattr("virtualenv.app_data.user_cache_dir", lambda **_kw: new_dir) - monkeypatch.setattr("virtualenv.app_data.user_data_dir", lambda **_kw: old_dir) - - def broken_move(_src: str, _dst: str) -> None: - msg = "permission denied" - raise OSError(msg) - - monkeypatch.setattr("virtualenv.app_data.shutil.move", broken_move) - - result = _cache_dir_with_migration() - assert result == old_dir - - @pytest.mark.parametrize("symlink_flag", [True, False]) - def test_symlink_app_data_survives_migration( - self, - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - symlink_flag: bool, # noqa: ARG002 - ) -> None: - old_dir = str(tmp_path / "old-data") - new_dir = str(tmp_path / "new-cache") - os.makedirs(old_dir) - wheel_img = tmp_path / "old-data" / "wheel" / "3.12" / "image" / "pip" - wheel_img.mkdir(parents=True) - (wheel_img / "pip.dist-info").mkdir() - (wheel_img / "pip.dist-info" / "METADATA").write_text("Name: pip") - - venv_dir = tmp_path / "my-venv" / "lib" / "site-packages" - venv_dir.mkdir(parents=True) - try: - os.symlink(str(wheel_img / "pip.dist-info"), str(venv_dir / "pip.dist-info")) - except OSError: - pytest.skip("symlinks not supported on this filesystem") - - monkeypatch.setattr("virtualenv.app_data.user_cache_dir", lambda **_kw: new_dir) - monkeypatch.setattr("virtualenv.app_data.user_data_dir", lambda **_kw: old_dir) - - result = _cache_dir_with_migration() - assert result == new_dir - assert (tmp_path / "new-cache" / "wheel" / "3.12" / "image" / "pip" / "pip.dist-info" / "METADATA").exists() diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..bc61f8cab --- /dev/null +++ b/tox.ini @@ -0,0 +1,36 @@ +[tox] +# env names must be a valid python binary name, unless they have a +# separate configuration +envlist = + python{2.6,2.7,3.3,3.4,3.5}, pypy{,3}, crosspython{2,3}, docs + +[testenv] +deps = + mock + pytest +commands = + py.test + +# Creating a python3 venv with a python2-based virtualenv +[testenv:crosspython2] +basepython=python2 +deps = +commands = + virtualenv -p python3 {envtmpdir}/{envname} + {envtmpdir}/{envname}/bin/python -V 2>&1 | grep "Python 3" + +# Creating a python2 venv with a python3-based virtualenv +[testenv:crosspython3] +basepython=python3 +deps = +commands = + virtualenv -p python2 {envtmpdir}/{envname} + {envtmpdir}/{envname}/bin/python -V 2>&1 | grep "Python 2" + +[testenv:docs] +deps = + sphinx + readme +commands = + sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html + python setup.py check -m -r -s diff --git a/tox.toml b/tox.toml deleted file mode 100644 index 49e3af1d0..000000000 --- a/tox.toml +++ /dev/null @@ -1,175 +0,0 @@ -requires = [ "tox>=4.45" ] -env_list = [ - "3.14", - "3.13", - "3.12", - "3.11", - "3.10", - "3.9", - "3.8", - "pypy3", - "3.13t", - "3.14t", - "coverage", - "docs", - "fix", - "graalpy", - "readme", - "rp", - "type", -] -skip_missing_interpreters = true - -[env_run_base] -description = "run tests with {env_name}" -package = "wheel" -wheel_build_env = ".pkg" -dependency_groups = [ "test" ] -pass_env = [ "CI_RUN", "PYTEST_*", "TERM" ] -set_env._COVERAGE_SRC = "{env_site_packages_dir}{/}virtualenv" -set_env.COVERAGE_FILE = "{work_dir}{/}.coverage.{env_name}" -set_env.COVERAGE_PROCESS_START = "{tox_root}{/}pyproject.toml" -set_env.PYTHONWARNDEFAULTENCODING = "1" -commands = [ - [ "coverage", "erase" ], - [ - "coverage", - "run", - "-m", - "pytest", - { replace = "posargs", default = [ - "--junitxml", - "{work_dir}{/}junit.{env_name}.xml", - "tests", - "--int", - "-n", - "auto", - "--dist", - "loadfile", - ], extend = true }, - ], - [ "coverage", "combine" ], - [ "coverage", "report", "--skip-covered", "--show-missing" ], - [ "coverage", "xml", "-o", "{work_dir}{/}coverage.{env_name}.xml" ], - [ - "coverage", - "html", - "-d", - "{env_tmp_dir}{/}htmlcov", - "--show-contexts", - "--title", - "virtualenv-{env_name}-coverage", - ], -] -uv_seed = true - -[env.docs] -description = "build documentation" -dependency_groups = [ "docs" ] -set_env.PYTHONUTF8 = "1" -commands = [ - [ "pre-commit", "run", "docstrfmt", "--all-files" ], - [ "proselint", "check", "docs" ], - [ - "sphinx-build", - "-d", - "{env_tmp_dir}{/}doctree", - "docs", - "{env:READTHEDOCS_OUTPUT:{env_tmp_dir}{/}docs_out}/html", - "--color", - "-b", - "html", - "-W", - ], - [ - "python", - "-c", - """\ - import pathlib; print(\"documentation available under file://\" + str(pathlib.Path(r\"{work_dir}\") / \"docs_out\" / \ - \"index.html\"))\ - """, - ], -] - -[env.fix] -description = "format the code base to adhere to our styles, and complain about what we cannot do automatically" -skip_install = true -dependency_groups = [ "lint" ] -commands = [ [ "pre-commit", "run", "--all-files", "--show-diff-on-failure" ] ] - -[env.graalpy] -commands = [ - [ - "pytest", - { replace = "posargs", default = [ - "--junitxml", - "{work_dir}{/}junit.{env_name}.xml", - "tests", - "--skip-slow", - ], extend = true }, - ], -] - -[env.readme] -description = "check that the long description is valid" -skip_install = true -dependency_groups = [ "pkg-meta" ] -commands = [ - [ "uv", "build", "--sdist", "--wheel", "--out-dir", "{env_tmp_dir}", "." ], - [ "twine", "check", "{env_tmp_dir}{/}*" ], - [ "check-wheel-contents", "--no-config", "{env_tmp_dir}" ], -] - -[env.rp] -commands = [ - [ - "pytest", - { replace = "posargs", default = [ - "--junitxml", - "{work_dir}{/}junit.{env_name}.xml", - "tests", - "--skip-slow", - ], extend = true }, - ], -] - -[env.type] -description = "run type checker (ty) against Python 3.14" -dependency_groups = [ "type" ] -commands = [ [ "python", "-m", "ty", "check", "src/virtualenv", "--python-version", "3.14" ] ] - -[env.dev] -description = "generate a DEV environment" -package = "editable" -dependency_groups = [ "dev" ] -commands = [ - [ "uv", "pip", "tree" ], - [ "python", "-c", "import sys; print(sys.executable)" ], -] - -[env.release] -description = "do a release, required posarg of the version number" -deps = [ "gitpython>=3.1.44", "packaging>=25", "pre-commit-uv>=4.1.4", "towncrier>=24.8" ] -change_dir = "{tox_root}{/}tasks" -commands = [ [ "python", "release.py", { replace = "posargs", extend = true } ] ] - -[env.upgrade] -description = "upgrade pip/wheels/setuptools to latest" -skip_install = true -deps = [ "ruff>=0.12.4" ] -pass_env = [ "UPGRADE_ADVISORY" ] -change_dir = "{tox_root}{/}tasks" -commands = [ [ "python", "upgrade_wheels.py" ] ] -uv_seed = true - -[env.zipapp] -description = "generate a zipapp" -skip_install = true -deps = [ "packaging>=25" ] -commands = [ [ "python", "tasks/make_zipapp.py" ] ] -uv_seed = true - -[env."type-3.8"] -description = "run type checker (ty) against Python 3.8" -commands = [ [ "python", "-m", "ty", "check", "src/virtualenv", "--python-version", "3.8" ] ] -base = [ "type" ] diff --git a/virtualenv.py b/virtualenv.py new file mode 100755 index 000000000..8f29a041e --- /dev/null +++ b/virtualenv.py @@ -0,0 +1,2332 @@ +#!/usr/bin/env python +"""Create a "virtual" Python installation""" + +import os +import sys + +# If we are running in a new interpreter to create a virtualenv, +# we do NOT want paths from our existing location interfering with anything, +# So we remove this file's directory from sys.path - most likely to be +# the previous interpreter's site-packages. Solves #705, #763, #779 +if os.environ.get('VIRTUALENV_INTERPRETER_RUNNING'): + for path in sys.path[:]: + if os.path.realpath(os.path.dirname(__file__)) == os.path.realpath(path): + sys.path.remove(path) + +import base64 +import codecs +import optparse +import re +import shutil +import logging +import zlib +import errno +import glob +import distutils.sysconfig +import struct +import subprocess +import pkgutil +import tempfile +import textwrap +from distutils.util import strtobool +from os.path import join + +try: + import ConfigParser +except ImportError: + import configparser as ConfigParser + +__version__ = "15.2.0.dev0" +virtualenv_version = __version__ # legacy + +if sys.version_info < (2, 6): + print('ERROR: %s' % sys.exc_info()[1]) + print('ERROR: this script requires Python 2.6 or greater.') + sys.exit(101) + +try: + basestring +except NameError: + basestring = str + +py_version = 'python%s.%s' % (sys.version_info[0], sys.version_info[1]) + +is_jython = sys.platform.startswith('java') +is_pypy = hasattr(sys, 'pypy_version_info') +is_win = (sys.platform == 'win32') +is_cygwin = (sys.platform == 'cygwin') +is_darwin = (sys.platform == 'darwin') +abiflags = getattr(sys, 'abiflags', '') + +user_dir = os.path.expanduser('~') +if is_win: + default_storage_dir = os.path.join(user_dir, 'virtualenv') +else: + default_storage_dir = os.path.join(user_dir, '.virtualenv') +default_config_file = os.path.join(default_storage_dir, 'virtualenv.ini') + +if is_pypy: + expected_exe = 'pypy' +elif is_jython: + expected_exe = 'jython' +else: + expected_exe = 'python' + +# Return a mapping of version -> Python executable +# Only provided for Windows, where the information in the registry is used +if not is_win: + def get_installed_pythons(): + return {} +else: + try: + import winreg + except ImportError: + import _winreg as winreg + + def get_installed_pythons(): + try: + python_core = winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, + "Software\\Python\\PythonCore") + except WindowsError: + # No registered Python installations + return {} + i = 0 + versions = [] + while True: + try: + versions.append(winreg.EnumKey(python_core, i)) + i = i + 1 + except WindowsError: + break + exes = dict() + for ver in versions: + try: + path = winreg.QueryValue(python_core, "%s\\InstallPath" % ver) + except WindowsError: + continue + exes[ver] = join(path, "python.exe") + + winreg.CloseKey(python_core) + + # Add the major versions + # Sort the keys, then repeatedly update the major version entry + # Last executable (i.e., highest version) wins with this approach + for ver in sorted(exes): + exes[ver[0]] = exes[ver] + + return exes + +REQUIRED_MODULES = ['os', 'posix', 'posixpath', 'nt', 'ntpath', 'genericpath', + 'fnmatch', 'locale', 'encodings', 'codecs', + 'stat', 'UserDict', 'readline', 'copy_reg', 'types', + 're', 'sre', 'sre_parse', 'sre_constants', 'sre_compile', + 'zlib'] + +REQUIRED_FILES = ['lib-dynload', 'config'] + +majver, minver = sys.version_info[:2] +if majver == 2: + if minver >= 6: + REQUIRED_MODULES.extend(['warnings', 'linecache', '_abcoll', 'abc']) + if minver >= 7: + REQUIRED_MODULES.extend(['_weakrefset']) +elif majver == 3: + # Some extra modules are needed for Python 3, but different ones + # for different versions. + REQUIRED_MODULES.extend([ + '_abcoll', 'warnings', 'linecache', 'abc', 'io', '_weakrefset', + 'copyreg', 'tempfile', 'random', '__future__', 'collections', + 'keyword', 'tarfile', 'shutil', 'struct', 'copy', 'tokenize', + 'token', 'functools', 'heapq', 'bisect', 'weakref', 'reprlib' + ]) + if minver >= 2: + REQUIRED_FILES[-1] = 'config-%s' % majver + if minver >= 3: + import sysconfig + platdir = sysconfig.get_config_var('PLATDIR') + REQUIRED_FILES.append(platdir) + REQUIRED_MODULES.extend([ + 'base64', '_dummy_thread', 'hashlib', 'hmac', + 'imp', 'importlib', 'rlcompleter' + ]) + if minver >= 4: + REQUIRED_MODULES.extend([ + 'operator', + '_collections_abc', + '_bootlocale', + ]) + if minver >= 6: + REQUIRED_MODULES.extend(['enum']) + +if is_pypy: + # these are needed to correctly display the exceptions that may happen + # during the bootstrap + REQUIRED_MODULES.extend(['traceback', 'linecache']) + + if majver == 3: + # _functools is needed to import locale during stdio initialization and + # needs to be copied on PyPy because it's not built in + REQUIRED_MODULES.append('_functools') + + +class Logger(object): + + """ + Logging object for use in command-line script. Allows ranges of + levels, to avoid some redundancy of displayed information. + """ + + DEBUG = logging.DEBUG + INFO = logging.INFO + NOTIFY = (logging.INFO+logging.WARN)/2 + WARN = WARNING = logging.WARN + ERROR = logging.ERROR + FATAL = logging.FATAL + + LEVELS = [DEBUG, INFO, NOTIFY, WARN, ERROR, FATAL] + + def __init__(self, consumers): + self.consumers = consumers + self.indent = 0 + self.in_progress = None + self.in_progress_hanging = False + + def debug(self, msg, *args, **kw): + self.log(self.DEBUG, msg, *args, **kw) + + def info(self, msg, *args, **kw): + self.log(self.INFO, msg, *args, **kw) + + def notify(self, msg, *args, **kw): + self.log(self.NOTIFY, msg, *args, **kw) + + def warn(self, msg, *args, **kw): + self.log(self.WARN, msg, *args, **kw) + + def error(self, msg, *args, **kw): + self.log(self.ERROR, msg, *args, **kw) + + def fatal(self, msg, *args, **kw): + self.log(self.FATAL, msg, *args, **kw) + + def log(self, level, msg, *args, **kw): + if args: + if kw: + raise TypeError( + "You may give positional or keyword arguments, not both") + args = args or kw + rendered = None + for consumer_level, consumer in self.consumers: + if self.level_matches(level, consumer_level): + if (self.in_progress_hanging + and consumer in (sys.stdout, sys.stderr)): + self.in_progress_hanging = False + sys.stdout.write('\n') + sys.stdout.flush() + if rendered is None: + if args: + rendered = msg % args + else: + rendered = msg + rendered = ' '*self.indent + rendered + if hasattr(consumer, 'write'): + consumer.write(rendered+'\n') + else: + consumer(rendered) + + def start_progress(self, msg): + assert not self.in_progress, ( + "Tried to start_progress(%r) while in_progress %r" + % (msg, self.in_progress)) + if self.level_matches(self.NOTIFY, self._stdout_level()): + sys.stdout.write(msg) + sys.stdout.flush() + self.in_progress_hanging = True + else: + self.in_progress_hanging = False + self.in_progress = msg + + def end_progress(self, msg='done.'): + assert self.in_progress, ( + "Tried to end_progress without start_progress") + if self.stdout_level_matches(self.NOTIFY): + if not self.in_progress_hanging: + # Some message has been printed out since start_progress + sys.stdout.write('...' + self.in_progress + msg + '\n') + sys.stdout.flush() + else: + sys.stdout.write(msg + '\n') + sys.stdout.flush() + self.in_progress = None + self.in_progress_hanging = False + + def show_progress(self): + """If we are in a progress scope, and no log messages have been + shown, write out another '.'""" + if self.in_progress_hanging: + sys.stdout.write('.') + sys.stdout.flush() + + def stdout_level_matches(self, level): + """Returns true if a message at this level will go to stdout""" + return self.level_matches(level, self._stdout_level()) + + def _stdout_level(self): + """Returns the level that stdout runs at""" + for level, consumer in self.consumers: + if consumer is sys.stdout: + return level + return self.FATAL + + def level_matches(self, level, consumer_level): + """ + >>> l = Logger([]) + >>> l.level_matches(3, 4) + False + >>> l.level_matches(3, 2) + True + >>> l.level_matches(slice(None, 3), 3) + False + >>> l.level_matches(slice(None, 3), 2) + True + >>> l.level_matches(slice(1, 3), 1) + True + >>> l.level_matches(slice(2, 3), 1) + False + """ + if isinstance(level, slice): + start, stop = level.start, level.stop + if start is not None and start > consumer_level: + return False + if stop is not None and stop <= consumer_level: + return False + return True + else: + return level >= consumer_level + + @classmethod + def level_for_integer(cls, level): + levels = cls.LEVELS + if level < 0: + return levels[0] + if level >= len(levels): + return levels[-1] + return levels[level] + +# create a silent logger just to prevent this from being undefined +# will be overridden with requested verbosity main() is called. +logger = Logger([(Logger.LEVELS[-1], sys.stdout)]) + +def mkdir(path): + if not os.path.exists(path): + logger.info('Creating %s', path) + os.makedirs(path) + else: + logger.info('Directory %s already exists', path) + +def copyfileordir(src, dest, symlink=True): + if os.path.isdir(src): + shutil.copytree(src, dest, symlink) + else: + shutil.copy2(src, dest) + +def copyfile(src, dest, symlink=True): + if not os.path.exists(src): + # Some bad symlink in the src + logger.warn('Cannot find file %s (bad symlink)', src) + return + if os.path.exists(dest): + logger.debug('File %s already exists', dest) + return + if not os.path.exists(os.path.dirname(dest)): + logger.info('Creating parent directories for %s', os.path.dirname(dest)) + os.makedirs(os.path.dirname(dest)) + if not os.path.islink(src): + srcpath = os.path.abspath(src) + else: + srcpath = os.readlink(src) + if symlink and hasattr(os, 'symlink') and not is_win: + logger.info('Symlinking %s', dest) + try: + os.symlink(srcpath, dest) + except (OSError, NotImplementedError): + logger.info('Symlinking failed, copying to %s', dest) + copyfileordir(src, dest, symlink) + else: + logger.info('Copying to %s', dest) + copyfileordir(src, dest, symlink) + +def writefile(dest, content, overwrite=True): + if not os.path.exists(dest): + logger.info('Writing %s', dest) + with open(dest, 'wb') as f: + f.write(content.encode('utf-8')) + return + else: + with open(dest, 'rb') as f: + c = f.read() + if c != content.encode("utf-8"): + if not overwrite: + logger.notify('File %s exists with different content; not overwriting', dest) + return + logger.notify('Overwriting %s with new content', dest) + with open(dest, 'wb') as f: + f.write(content.encode('utf-8')) + else: + logger.info('Content %s already in place', dest) + +def rmtree(dir): + if os.path.exists(dir): + logger.notify('Deleting tree %s', dir) + shutil.rmtree(dir) + else: + logger.info('Do not need to delete %s; already gone', dir) + +def make_exe(fn): + if hasattr(os, 'chmod'): + oldmode = os.stat(fn).st_mode & 0xFFF # 0o7777 + newmode = (oldmode | 0x16D) & 0xFFF # 0o555, 0o7777 + os.chmod(fn, newmode) + logger.info('Changed mode of %s to %s', fn, oct(newmode)) + +def _find_file(filename, dirs): + for dir in reversed(dirs): + files = glob.glob(os.path.join(dir, filename)) + if files and os.path.isfile(files[0]): + return True, files[0] + return False, filename + +def file_search_dirs(): + here = os.path.dirname(os.path.abspath(__file__)) + dirs = [here, join(here, 'virtualenv_support')] + if os.path.splitext(os.path.dirname(__file__))[0] != 'virtualenv': + # Probably some boot script; just in case virtualenv is installed... + try: + import virtualenv + except ImportError: + pass + else: + dirs.append(os.path.join( + os.path.dirname(virtualenv.__file__), 'virtualenv_support')) + return [d for d in dirs if os.path.isdir(d)] + + +class UpdatingDefaultsHelpFormatter(optparse.IndentedHelpFormatter): + """ + Custom help formatter for use in ConfigOptionParser that updates + the defaults before expanding them, allowing them to show up correctly + in the help listing + """ + def expand_default(self, option): + if self.parser is not None: + self.parser.update_defaults(self.parser.defaults) + return optparse.IndentedHelpFormatter.expand_default(self, option) + + +class ConfigOptionParser(optparse.OptionParser): + """ + Custom option parser which updates its defaults by checking the + configuration files and environmental variables + """ + def __init__(self, *args, **kwargs): + self.config = ConfigParser.RawConfigParser() + self.files = self.get_config_files() + self.config.read(self.files) + optparse.OptionParser.__init__(self, *args, **kwargs) + + def get_config_files(self): + config_file = os.environ.get('VIRTUALENV_CONFIG_FILE', False) + if config_file and os.path.exists(config_file): + return [config_file] + return [default_config_file] + + def update_defaults(self, defaults): + """ + Updates the given defaults with values from the config files and + the environ. Does a little special handling for certain types of + options (lists). + """ + # Then go and look for the other sources of configuration: + config = {} + # 1. config files + config.update(dict(self.get_config_section('virtualenv'))) + # 2. environmental variables + config.update(dict(self.get_environ_vars())) + # Then set the options with those values + for key, val in config.items(): + key = key.replace('_', '-') + if not key.startswith('--'): + key = '--%s' % key # only prefer long opts + option = self.get_option(key) + if option is not None: + # ignore empty values + if not val: + continue + # handle multiline configs + if option.action == 'append': + val = val.split() + else: + option.nargs = 1 + if option.action == 'store_false': + val = not strtobool(val) + elif option.action in ('store_true', 'count'): + val = strtobool(val) + try: + val = option.convert_value(key, val) + except optparse.OptionValueError: + e = sys.exc_info()[1] + print("An error occurred during configuration: %s" % e) + sys.exit(3) + defaults[option.dest] = val + return defaults + + def get_config_section(self, name): + """ + Get a section of a configuration + """ + if self.config.has_section(name): + return self.config.items(name) + return [] + + def get_environ_vars(self, prefix='VIRTUALENV_'): + """ + Returns a generator with all environmental vars with prefix VIRTUALENV + """ + for key, val in os.environ.items(): + if key.startswith(prefix): + yield (key.replace(prefix, '').lower(), val) + + def get_default_values(self): + """ + Overridding to make updating the defaults after instantiation of + the option parser possible, update_defaults() does the dirty work. + """ + if not self.process_default_values: + # Old, pre-Optik 1.5 behaviour. + return optparse.Values(self.defaults) + + defaults = self.update_defaults(self.defaults.copy()) # ours + for option in self._get_all_options(): + default = defaults.get(option.dest) + if isinstance(default, basestring): + opt_str = option.get_opt_string() + defaults[option.dest] = option.check_value(opt_str, default) + return optparse.Values(defaults) + + +def main(): + parser = ConfigOptionParser( + version=virtualenv_version, + usage="%prog [OPTIONS] DEST_DIR", + formatter=UpdatingDefaultsHelpFormatter()) + + parser.add_option( + '-v', '--verbose', + action='count', + dest='verbose', + default=0, + help="Increase verbosity.") + + parser.add_option( + '-q', '--quiet', + action='count', + dest='quiet', + default=0, + help='Decrease verbosity.') + + parser.add_option( + '-p', '--python', + dest='python', + metavar='PYTHON_EXE', + help='The Python interpreter to use, e.g., --python=python2.5 will use the python2.5 ' + 'interpreter to create the new environment. The default is the interpreter that ' + 'virtualenv was installed with (%s)' % sys.executable) + + parser.add_option( + '--clear', + dest='clear', + action='store_true', + help="Clear out the non-root install and start from scratch.") + + parser.set_defaults(system_site_packages=False) + parser.add_option( + '--no-site-packages', + dest='system_site_packages', + action='store_false', + help="DEPRECATED. Retained only for backward compatibility. " + "Not having access to global site-packages is now the default behavior.") + + parser.add_option( + '--system-site-packages', + dest='system_site_packages', + action='store_true', + help="Give the virtual environment access to the global site-packages.") + + parser.add_option( + '--always-copy', + dest='symlink', + action='store_false', + default=True, + help="Always copy files rather than symlinking.") + + parser.add_option( + '--unzip-setuptools', + dest='unzip_setuptools', + action='store_true', + help="Unzip Setuptools when installing it.") + + parser.add_option( + '--relocatable', + dest='relocatable', + action='store_true', + help='Make an EXISTING virtualenv environment relocatable. ' + 'This fixes up scripts and makes all .pth files relative.') + + parser.add_option( + '--no-setuptools', + dest='no_setuptools', + action='store_true', + help='Do not install setuptools in the new virtualenv.') + + parser.add_option( + '--no-pip', + dest='no_pip', + action='store_true', + help='Do not install pip in the new virtualenv.') + + parser.add_option( + '--no-wheel', + dest='no_wheel', + action='store_true', + help='Do not install wheel in the new virtualenv.') + + default_search_dirs = file_search_dirs() + parser.add_option( + '--extra-search-dir', + dest="search_dirs", + action="append", + metavar='DIR', + default=default_search_dirs, + help="Directory to look for setuptools/pip distributions in. " + "This option can be used multiple times.") + + parser.add_option( + "--download", + dest="download", + default=True, + action="store_true", + help="Download preinstalled packages from PyPI.", + ) + + parser.add_option( + "--no-download", + '--never-download', + dest="download", + action="store_false", + help="Do not download preinstalled packages from PyPI.", + ) + + parser.add_option( + '--prompt', + dest='prompt', + help='Provides an alternative prompt prefix for this environment.') + + parser.add_option( + '--setuptools', + dest='setuptools', + action='store_true', + help="DEPRECATED. Retained only for backward compatibility. This option has no effect.") + + parser.add_option( + '--distribute', + dest='distribute', + action='store_true', + help="DEPRECATED. Retained only for backward compatibility. This option has no effect.") + + if 'extend_parser' in globals(): + extend_parser(parser) + + options, args = parser.parse_args() + + global logger + + if 'adjust_options' in globals(): + adjust_options(options, args) + + verbosity = options.verbose - options.quiet + logger = Logger([(Logger.level_for_integer(2 - verbosity), sys.stdout)]) + + if options.python and not os.environ.get('VIRTUALENV_INTERPRETER_RUNNING'): + env = os.environ.copy() + interpreter = resolve_interpreter(options.python) + if interpreter == sys.executable: + logger.warn('Already using interpreter %s' % interpreter) + else: + logger.notify('Running virtualenv with interpreter %s' % interpreter) + env['VIRTUALENV_INTERPRETER_RUNNING'] = 'true' + file = __file__ + if file.endswith('.pyc'): + file = file[:-1] + popen = subprocess.Popen([interpreter, file] + sys.argv[1:], env=env) + raise SystemExit(popen.wait()) + + if not args: + print('You must provide a DEST_DIR') + parser.print_help() + sys.exit(2) + if len(args) > 1: + print('There must be only one argument: DEST_DIR (you gave %s)' % ( + ' '.join(args))) + parser.print_help() + sys.exit(2) + + home_dir = args[0] + + if os.path.exists(home_dir) and os.path.isfile(home_dir): + logger.fatal('ERROR: File already exists and is not a directory.') + logger.fatal('Please provide a different path or delete the file.') + sys.exit(3) + + if os.environ.get('WORKING_ENV'): + logger.fatal('ERROR: you cannot run virtualenv while in a workingenv') + logger.fatal('Please deactivate your workingenv, then re-run this script') + sys.exit(3) + + if 'PYTHONHOME' in os.environ: + logger.warn('PYTHONHOME is set. You *must* activate the virtualenv before using it') + del os.environ['PYTHONHOME'] + + if options.relocatable: + make_environment_relocatable(home_dir) + return + + create_environment(home_dir, + site_packages=options.system_site_packages, + clear=options.clear, + unzip_setuptools=options.unzip_setuptools, + prompt=options.prompt, + search_dirs=options.search_dirs, + download=options.download, + no_setuptools=options.no_setuptools, + no_pip=options.no_pip, + no_wheel=options.no_wheel, + symlink=options.symlink) + if 'after_install' in globals(): + after_install(options, home_dir) + +def call_subprocess(cmd, show_stdout=True, + filter_stdout=None, cwd=None, + raise_on_returncode=True, extra_env=None, + remove_from_env=None, stdin=None): + cmd_parts = [] + for part in cmd: + if len(part) > 45: + part = part[:20]+"..."+part[-20:] + if ' ' in part or '\n' in part or '"' in part or "'" in part: + part = '"%s"' % part.replace('"', '\\"') + if hasattr(part, 'decode'): + try: + part = part.decode(sys.getdefaultencoding()) + except UnicodeDecodeError: + part = part.decode(sys.getfilesystemencoding()) + cmd_parts.append(part) + cmd_desc = ' '.join(cmd_parts) + if show_stdout: + stdout = None + else: + stdout = subprocess.PIPE + logger.debug("Running command %s" % cmd_desc) + if extra_env or remove_from_env: + env = os.environ.copy() + if extra_env: + env.update(extra_env) + if remove_from_env: + for varname in remove_from_env: + env.pop(varname, None) + else: + env = None + try: + proc = subprocess.Popen( + cmd, stderr=subprocess.STDOUT, + stdin=None if stdin is None else subprocess.PIPE, + stdout=stdout, + cwd=cwd, env=env) + except Exception: + e = sys.exc_info()[1] + logger.fatal( + "Error %s while executing command %s" % (e, cmd_desc)) + raise + all_output = [] + if stdout is not None: + if stdin is not None: + proc.stdin.write(stdin) + proc.stdin.close() + + stdout = proc.stdout + encoding = sys.getdefaultencoding() + fs_encoding = sys.getfilesystemencoding() + while 1: + line = stdout.readline() + try: + line = line.decode(encoding) + except UnicodeDecodeError: + line = line.decode(fs_encoding) + if not line: + break + line = line.rstrip() + all_output.append(line) + if filter_stdout: + level = filter_stdout(line) + if isinstance(level, tuple): + level, line = level + logger.log(level, line) + if not logger.stdout_level_matches(level): + logger.show_progress() + else: + logger.info(line) + else: + proc.communicate(stdin) + proc.wait() + if proc.returncode: + if raise_on_returncode: + if all_output: + logger.notify('Complete output from command %s:' % cmd_desc) + logger.notify('\n'.join(all_output) + '\n----------------------------------------') + raise OSError( + "Command %s failed with error code %s" + % (cmd_desc, proc.returncode)) + else: + logger.warn( + "Command %s had error code %s" + % (cmd_desc, proc.returncode)) + +def filter_install_output(line): + if line.strip().startswith('running'): + return Logger.INFO + return Logger.DEBUG + +def find_wheels(projects, search_dirs): + """Find wheels from which we can import PROJECTS. + + Scan through SEARCH_DIRS for a wheel for each PROJECT in turn. Return + a list of the first wheel found for each PROJECT + """ + + wheels = [] + + # Look through SEARCH_DIRS for the first suitable wheel. Don't bother + # about version checking here, as this is simply to get something we can + # then use to install the correct version. + for project in projects: + for dirname in search_dirs: + # This relies on only having "universal" wheels available. + # The pattern could be tightened to require -py2.py3-none-any.whl. + files = glob.glob(os.path.join(dirname, project + '-*.whl')) + if files: + wheels.append(os.path.abspath(files[0])) + break + else: + # We're out of luck, so quit with a suitable error + logger.fatal('Cannot find a wheel for %s' % (project,)) + + return wheels + +def install_wheel(project_names, py_executable, search_dirs=None, + download=False): + if search_dirs is None: + search_dirs = file_search_dirs() + + wheels = find_wheels(['setuptools', 'pip'], search_dirs) + pythonpath = os.pathsep.join(wheels) + + # PIP_FIND_LINKS uses space as the path separator and thus cannot have paths + # with spaces in them. Convert any of those to local file:// URL form. + try: + from urlparse import urljoin + from urllib import pathname2url + except ImportError: + from urllib.parse import urljoin + from urllib.request import pathname2url + def space_path2url(p): + if ' ' not in p: + return p + return urljoin('file:', pathname2url(os.path.abspath(p))) + findlinks = ' '.join(space_path2url(d) for d in search_dirs) + + SCRIPT = textwrap.dedent(""" + import sys + import pkgutil + import tempfile + import os + + import pip + + cert_data = pkgutil.get_data("pip._vendor.requests", "cacert.pem") + if cert_data is not None: + cert_file = tempfile.NamedTemporaryFile(delete=False) + cert_file.write(cert_data) + cert_file.close() + else: + cert_file = None + + try: + args = ["install", "--ignore-installed"] + if cert_file is not None: + args += ["--cert", cert_file.name] + args += sys.argv[1:] + + sys.exit(pip.main(args)) + finally: + if cert_file is not None: + os.remove(cert_file.name) + """).encode("utf8") + + cmd = [py_executable, '-'] + project_names + logger.start_progress('Installing %s...' % (', '.join(project_names))) + logger.indent += 2 + + env = { + "PYTHONPATH": pythonpath, + "JYTHONPATH": pythonpath, # for Jython < 3.x + "PIP_FIND_LINKS": findlinks, + "PIP_USE_WHEEL": "1", + "PIP_ONLY_BINARY": ":all:", + "PIP_USER": "0", + } + + if not download: + env["PIP_NO_INDEX"] = "1" + + try: + call_subprocess(cmd, show_stdout=False, extra_env=env, stdin=SCRIPT) + finally: + logger.indent -= 2 + logger.end_progress() + + +def create_environment(home_dir, site_packages=False, clear=False, + unzip_setuptools=False, + prompt=None, search_dirs=None, download=False, + no_setuptools=False, no_pip=False, no_wheel=False, + symlink=True): + """ + Creates a new environment in ``home_dir``. + + If ``site_packages`` is true, then the global ``site-packages/`` + directory will be on the path. + + If ``clear`` is true (default False) then the environment will + first be cleared. + """ + home_dir, lib_dir, inc_dir, bin_dir = path_locations(home_dir) + + py_executable = os.path.abspath(install_python( + home_dir, lib_dir, inc_dir, bin_dir, + site_packages=site_packages, clear=clear, symlink=symlink)) + + install_distutils(home_dir) + + to_install = [] + + if not no_setuptools: + to_install.append('setuptools') + + if not no_pip: + to_install.append('pip') + + if not no_wheel: + to_install.append('wheel') + + if to_install: + install_wheel( + to_install, + py_executable, + search_dirs, + download=download, + ) + + install_activate(home_dir, bin_dir, prompt) + + install_python_config(home_dir, bin_dir, prompt) + +def is_executable_file(fpath): + return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + +def path_locations(home_dir): + """Return the path locations for the environment (where libraries are, + where scripts go, etc)""" + home_dir = os.path.abspath(home_dir) + # XXX: We'd use distutils.sysconfig.get_python_inc/lib but its + # prefix arg is broken: http://bugs.python.org/issue3386 + if is_win: + # Windows has lots of problems with executables with spaces in + # the name; this function will remove them (using the ~1 + # format): + mkdir(home_dir) + if ' ' in home_dir: + import ctypes + GetShortPathName = ctypes.windll.kernel32.GetShortPathNameW + size = max(len(home_dir)+1, 256) + buf = ctypes.create_unicode_buffer(size) + try: + u = unicode + except NameError: + u = str + ret = GetShortPathName(u(home_dir), buf, size) + if not ret: + print('Error: the path "%s" has a space in it' % home_dir) + print('We could not determine the short pathname for it.') + print('Exiting.') + sys.exit(3) + home_dir = str(buf.value) + lib_dir = join(home_dir, 'Lib') + inc_dir = join(home_dir, 'Include') + bin_dir = join(home_dir, 'Scripts') + if is_jython: + lib_dir = join(home_dir, 'Lib') + inc_dir = join(home_dir, 'Include') + bin_dir = join(home_dir, 'bin') + elif is_pypy: + lib_dir = home_dir + inc_dir = join(home_dir, 'include') + bin_dir = join(home_dir, 'bin') + elif not is_win: + lib_dir = join(home_dir, 'lib', py_version) + inc_dir = join(home_dir, 'include', py_version + abiflags) + bin_dir = join(home_dir, 'bin') + return home_dir, lib_dir, inc_dir, bin_dir + + +def change_prefix(filename, dst_prefix): + prefixes = [sys.prefix] + + if is_darwin: + prefixes.extend(( + os.path.join("/Library/Python", sys.version[:3], "site-packages"), + os.path.join(sys.prefix, "Extras", "lib", "python"), + os.path.join("~", "Library", "Python", sys.version[:3], "site-packages"), + # Python 2.6 no-frameworks + os.path.join("~", ".local", "lib","python", sys.version[:3], "site-packages"), + # System Python 2.7 on OSX Mountain Lion + os.path.join("~", "Library", "Python", sys.version[:3], "lib", "python", "site-packages"))) + + if hasattr(sys, 'real_prefix'): + prefixes.append(sys.real_prefix) + if hasattr(sys, 'base_prefix'): + prefixes.append(sys.base_prefix) + prefixes = list(map(os.path.expanduser, prefixes)) + prefixes = list(map(os.path.abspath, prefixes)) + # Check longer prefixes first so we don't split in the middle of a filename + prefixes = sorted(prefixes, key=len, reverse=True) + filename = os.path.abspath(filename) + # On Windows, make sure drive letter is uppercase + if is_win and filename[0] in 'abcdefghijklmnopqrstuvwxyz': + filename = filename[0].upper() + filename[1:] + for i, prefix in enumerate(prefixes): + if is_win and prefix[0] in 'abcdefghijklmnopqrstuvwxyz': + prefixes[i] = prefix[0].upper() + prefix[1:] + for src_prefix in prefixes: + if filename.startswith(src_prefix): + _, relpath = filename.split(src_prefix, 1) + if src_prefix != os.sep: # sys.prefix == "/" + assert relpath[0] == os.sep + relpath = relpath[1:] + return join(dst_prefix, relpath) + assert False, "Filename %s does not start with any of these prefixes: %s" % \ + (filename, prefixes) + +def copy_required_modules(dst_prefix, symlink): + import imp + + for modname in REQUIRED_MODULES: + if modname in sys.builtin_module_names: + logger.info("Ignoring built-in bootstrap module: %s" % modname) + continue + try: + f, filename, _ = imp.find_module(modname) + except ImportError: + logger.info("Cannot import bootstrap module: %s" % modname) + else: + if f is not None: + f.close() + # special-case custom readline.so on OS X, but not for pypy: + if modname == 'readline' and sys.platform == 'darwin' and not ( + is_pypy or filename.endswith(join('lib-dynload', 'readline.so'))): + dst_filename = join(dst_prefix, 'lib', 'python%s' % sys.version[:3], 'readline.so') + elif modname == 'readline' and sys.platform == 'win32': + # special-case for Windows, where readline is not a + # standard module, though it may have been installed in + # site-packages by a third-party package + pass + else: + dst_filename = change_prefix(filename, dst_prefix) + copyfile(filename, dst_filename, symlink) + if filename.endswith('.pyc'): + pyfile = filename[:-1] + if os.path.exists(pyfile): + copyfile(pyfile, dst_filename[:-1], symlink) + +def copy_tcltk(src, dest, symlink): + """ copy tcl/tk libraries on Windows (issue #93) """ + for libversion in '8.5', '8.6': + for libname in 'tcl', 'tk': + srcdir = join(src, 'tcl', libname + libversion) + destdir = join(dest, 'tcl', libname + libversion) + # Only copy the dirs from the above combinations that exist + if os.path.exists(srcdir) and not os.path.exists(destdir): + copyfileordir(srcdir, destdir, symlink) + + +def subst_path(prefix_path, prefix, home_dir): + prefix_path = os.path.normpath(prefix_path) + prefix = os.path.normpath(prefix) + home_dir = os.path.normpath(home_dir) + if not prefix_path.startswith(prefix): + logger.warn('Path not in prefix %r %r', prefix_path, prefix) + return + return prefix_path.replace(prefix, home_dir, 1) + + +def install_python(home_dir, lib_dir, inc_dir, bin_dir, site_packages, clear, symlink=True): + """Install just the base environment, no distutils patches etc""" + if sys.executable.startswith(bin_dir): + print('Please use the *system* python to run this script') + return + + if clear: + rmtree(lib_dir) + ## FIXME: why not delete it? + ## Maybe it should delete everything with #!/path/to/venv/python in it + logger.notify('Not deleting %s', bin_dir) + + if hasattr(sys, 'real_prefix'): + logger.notify('Using real prefix %r' % sys.real_prefix) + prefix = sys.real_prefix + elif hasattr(sys, 'base_prefix'): + logger.notify('Using base prefix %r' % sys.base_prefix) + prefix = sys.base_prefix + else: + prefix = sys.prefix + mkdir(lib_dir) + fix_lib64(lib_dir, symlink) + stdlib_dirs = [os.path.dirname(os.__file__)] + if is_win: + stdlib_dirs.append(join(os.path.dirname(stdlib_dirs[0]), 'DLLs')) + elif is_darwin: + stdlib_dirs.append(join(stdlib_dirs[0], 'site-packages')) + if hasattr(os, 'symlink'): + logger.info('Symlinking Python bootstrap modules') + else: + logger.info('Copying Python bootstrap modules') + logger.indent += 2 + try: + # copy required files... + for stdlib_dir in stdlib_dirs: + if not os.path.isdir(stdlib_dir): + continue + for fn in os.listdir(stdlib_dir): + bn = os.path.splitext(fn)[0] + if fn != 'site-packages' and bn in REQUIRED_FILES: + copyfile(join(stdlib_dir, fn), join(lib_dir, fn), symlink) + # ...and modules + copy_required_modules(home_dir, symlink) + finally: + logger.indent -= 2 + # ...copy tcl/tk + if is_win: + copy_tcltk(prefix, home_dir, symlink) + mkdir(join(lib_dir, 'site-packages')) + import site + site_filename = site.__file__ + if site_filename.endswith('.pyc') or site_filename.endswith('.pyo'): + site_filename = site_filename[:-1] + elif site_filename.endswith('$py.class'): + site_filename = site_filename.replace('$py.class', '.py') + site_filename_dst = change_prefix(site_filename, home_dir) + site_dir = os.path.dirname(site_filename_dst) + writefile(site_filename_dst, SITE_PY) + writefile(join(site_dir, 'orig-prefix.txt'), prefix) + site_packages_filename = join(site_dir, 'no-global-site-packages.txt') + if not site_packages: + writefile(site_packages_filename, '') + + if is_pypy or is_win: + stdinc_dir = join(prefix, 'include') + else: + stdinc_dir = join(prefix, 'include', py_version + abiflags) + if os.path.exists(stdinc_dir): + copyfile(stdinc_dir, inc_dir, symlink) + else: + logger.debug('No include dir %s' % stdinc_dir) + + platinc_dir = distutils.sysconfig.get_python_inc(plat_specific=1) + if platinc_dir != stdinc_dir: + platinc_dest = distutils.sysconfig.get_python_inc( + plat_specific=1, prefix=home_dir) + if platinc_dir == platinc_dest: + # Do platinc_dest manually due to a CPython bug; + # not http://bugs.python.org/issue3386 but a close cousin + platinc_dest = subst_path(platinc_dir, prefix, home_dir) + if platinc_dest: + # PyPy's stdinc_dir and prefix are relative to the original binary + # (traversing virtualenvs), whereas the platinc_dir is relative to + # the inner virtualenv and ignores the prefix argument. + # This seems more evolved than designed. + copyfile(platinc_dir, platinc_dest, symlink) + + # pypy never uses exec_prefix, just ignore it + if sys.exec_prefix != prefix and not is_pypy: + if is_win: + exec_dir = join(sys.exec_prefix, 'lib') + elif is_jython: + exec_dir = join(sys.exec_prefix, 'Lib') + else: + exec_dir = join(sys.exec_prefix, 'lib', py_version) + for fn in os.listdir(exec_dir): + copyfile(join(exec_dir, fn), join(lib_dir, fn), symlink) + + if is_jython: + # Jython has either jython-dev.jar and javalib/ dir, or just + # jython.jar + for name in 'jython-dev.jar', 'javalib', 'jython.jar': + src = join(prefix, name) + if os.path.exists(src): + copyfile(src, join(home_dir, name), symlink) + # XXX: registry should always exist after Jython 2.5rc1 + src = join(prefix, 'registry') + if os.path.exists(src): + copyfile(src, join(home_dir, 'registry'), symlink=False) + copyfile(join(prefix, 'cachedir'), join(home_dir, 'cachedir'), + symlink=False) + + mkdir(bin_dir) + py_executable = join(bin_dir, os.path.basename(sys.executable)) + if 'Python.framework' in prefix: + # OS X framework builds cause validation to break + # https://github.com/pypa/virtualenv/issues/322 + if os.environ.get('__PYVENV_LAUNCHER__'): + del os.environ["__PYVENV_LAUNCHER__"] + if re.search(r'/Python(?:-32|-64)*$', py_executable): + # The name of the python executable is not quite what + # we want, rename it. + py_executable = os.path.join( + os.path.dirname(py_executable), 'python') + + logger.notify('New %s executable in %s', expected_exe, py_executable) + pcbuild_dir = os.path.dirname(sys.executable) + pyd_pth = os.path.join(lib_dir, 'site-packages', 'virtualenv_builddir_pyd.pth') + if is_win and os.path.exists(os.path.join(pcbuild_dir, 'build.bat')): + logger.notify('Detected python running from build directory %s', pcbuild_dir) + logger.notify('Writing .pth file linking to build directory for *.pyd files') + writefile(pyd_pth, pcbuild_dir) + else: + pcbuild_dir = None + if os.path.exists(pyd_pth): + logger.info('Deleting %s (not Windows env or not build directory python)' % pyd_pth) + os.unlink(pyd_pth) + + if sys.executable != py_executable: + ## FIXME: could I just hard link? + executable = sys.executable + shutil.copyfile(executable, py_executable) + make_exe(py_executable) + if is_win or is_cygwin: + pythonw = os.path.join(os.path.dirname(sys.executable), 'pythonw.exe') + if os.path.exists(pythonw): + logger.info('Also created pythonw.exe') + shutil.copyfile(pythonw, os.path.join(os.path.dirname(py_executable), 'pythonw.exe')) + python_d = os.path.join(os.path.dirname(sys.executable), 'python_d.exe') + python_d_dest = os.path.join(os.path.dirname(py_executable), 'python_d.exe') + if os.path.exists(python_d): + logger.info('Also created python_d.exe') + shutil.copyfile(python_d, python_d_dest) + elif os.path.exists(python_d_dest): + logger.info('Removed python_d.exe as it is no longer at the source') + os.unlink(python_d_dest) + # we need to copy the DLL to enforce that windows will load the correct one. + # may not exist if we are cygwin. + py_executable_dll = 'python%s%s.dll' % ( + sys.version_info[0], sys.version_info[1]) + py_executable_dll_d = 'python%s%s_d.dll' % ( + sys.version_info[0], sys.version_info[1]) + pythondll = os.path.join(os.path.dirname(sys.executable), py_executable_dll) + pythondll_d = os.path.join(os.path.dirname(sys.executable), py_executable_dll_d) + pythondll_d_dest = os.path.join(os.path.dirname(py_executable), py_executable_dll_d) + if os.path.exists(pythondll): + logger.info('Also created %s' % py_executable_dll) + shutil.copyfile(pythondll, os.path.join(os.path.dirname(py_executable), py_executable_dll)) + if os.path.exists(pythondll_d): + logger.info('Also created %s' % py_executable_dll_d) + shutil.copyfile(pythondll_d, pythondll_d_dest) + elif os.path.exists(pythondll_d_dest): + logger.info('Removed %s as the source does not exist' % pythondll_d_dest) + os.unlink(pythondll_d_dest) + if is_pypy: + # make a symlink python --> pypy-c + python_executable = os.path.join(os.path.dirname(py_executable), 'python') + if sys.platform in ('win32', 'cygwin'): + python_executable += '.exe' + logger.info('Also created executable %s' % python_executable) + copyfile(py_executable, python_executable, symlink) + + if is_win: + for name in ['libexpat.dll', 'libpypy.dll', 'libpypy-c.dll', + 'libeay32.dll', 'ssleay32.dll', 'sqlite3.dll', + 'tcl85.dll', 'tk85.dll']: + src = join(prefix, name) + if os.path.exists(src): + copyfile(src, join(bin_dir, name), symlink) + + for d in sys.path: + if d.endswith('lib_pypy'): + break + else: + logger.fatal('Could not find lib_pypy in sys.path') + raise SystemExit(3) + logger.info('Copying lib_pypy') + copyfile(d, os.path.join(home_dir, 'lib_pypy'), symlink) + + if os.path.splitext(os.path.basename(py_executable))[0] != expected_exe: + secondary_exe = os.path.join(os.path.dirname(py_executable), + expected_exe) + py_executable_ext = os.path.splitext(py_executable)[1] + if py_executable_ext.lower() == '.exe': + # python2.4 gives an extension of '.4' :P + secondary_exe += py_executable_ext + if os.path.exists(secondary_exe): + logger.warn('Not overwriting existing %s script %s (you must use %s)' + % (expected_exe, secondary_exe, py_executable)) + else: + logger.notify('Also creating executable in %s' % secondary_exe) + shutil.copyfile(sys.executable, secondary_exe) + make_exe(secondary_exe) + + if '.framework' in prefix: + if 'Python.framework' in prefix: + logger.debug('MacOSX Python framework detected') + # Make sure we use the embedded interpreter inside + # the framework, even if sys.executable points to + # the stub executable in ${sys.prefix}/bin + # See http://groups.google.com/group/python-virtualenv/ + # browse_thread/thread/17cab2f85da75951 + original_python = os.path.join( + prefix, 'Resources/Python.app/Contents/MacOS/Python') + if 'EPD' in prefix: + logger.debug('EPD framework detected') + original_python = os.path.join(prefix, 'bin/python') + shutil.copy(original_python, py_executable) + + # Copy the framework's dylib into the virtual + # environment + virtual_lib = os.path.join(home_dir, '.Python') + + if os.path.exists(virtual_lib): + os.unlink(virtual_lib) + copyfile( + os.path.join(prefix, 'Python'), + virtual_lib, + symlink) + + # And then change the install_name of the copied python executable + try: + mach_o_change(py_executable, + os.path.join(prefix, 'Python'), + '@executable_path/../.Python') + except: + e = sys.exc_info()[1] + logger.warn("Could not call mach_o_change: %s. " + "Trying to call install_name_tool instead." % e) + try: + call_subprocess( + ["install_name_tool", "-change", + os.path.join(prefix, 'Python'), + '@executable_path/../.Python', + py_executable]) + except: + logger.fatal("Could not call install_name_tool -- you must " + "have Apple's development tools installed") + raise + + if not is_win: + # Ensure that 'python', 'pythonX' and 'pythonX.Y' all exist + py_exe_version_major = 'python%s' % sys.version_info[0] + py_exe_version_major_minor = 'python%s.%s' % ( + sys.version_info[0], sys.version_info[1]) + py_exe_no_version = 'python' + required_symlinks = [ py_exe_no_version, py_exe_version_major, + py_exe_version_major_minor ] + + py_executable_base = os.path.basename(py_executable) + + if py_executable_base in required_symlinks: + # Don't try to symlink to yourself. + required_symlinks.remove(py_executable_base) + + for pth in required_symlinks: + full_pth = join(bin_dir, pth) + if os.path.exists(full_pth): + os.unlink(full_pth) + if symlink: + os.symlink(py_executable_base, full_pth) + else: + copyfile(py_executable, full_pth, symlink) + + cmd = [py_executable, '-c', 'import sys;out=sys.stdout;' + 'getattr(out, "buffer", out).write(sys.prefix.encode("utf-8"))'] + logger.info('Testing executable with %s %s "%s"' % tuple(cmd)) + try: + proc = subprocess.Popen(cmd, + stdout=subprocess.PIPE) + proc_stdout, proc_stderr = proc.communicate() + except OSError: + e = sys.exc_info()[1] + if e.errno == errno.EACCES: + logger.fatal('ERROR: The executable %s could not be run: %s' % (py_executable, e)) + sys.exit(100) + else: + raise e + + proc_stdout = proc_stdout.strip().decode("utf-8") + proc_stdout = os.path.normcase(os.path.abspath(proc_stdout)) + norm_home_dir = os.path.normcase(os.path.abspath(home_dir)) + if hasattr(norm_home_dir, 'decode'): + norm_home_dir = norm_home_dir.decode(sys.getfilesystemencoding()) + if proc_stdout != norm_home_dir: + logger.fatal( + 'ERROR: The executable %s is not functioning' % py_executable) + logger.fatal( + 'ERROR: It thinks sys.prefix is %r (should be %r)' + % (proc_stdout, norm_home_dir)) + logger.fatal( + 'ERROR: virtualenv is not compatible with this system or executable') + if is_win: + logger.fatal( + 'Note: some Windows users have reported this error when they ' + 'installed Python for "Only this user" or have multiple ' + 'versions of Python installed. Copying the appropriate ' + 'PythonXX.dll to the virtualenv Scripts/ directory may fix ' + 'this problem.') + sys.exit(100) + else: + logger.info('Got sys.prefix result: %r' % proc_stdout) + + pydistutils = os.path.expanduser('~/.pydistutils.cfg') + if os.path.exists(pydistutils): + logger.notify('Please make sure you remove any previous custom paths from ' + 'your %s file.' % pydistutils) + ## FIXME: really this should be calculated earlier + + fix_local_scheme(home_dir, symlink) + + if site_packages: + if os.path.exists(site_packages_filename): + logger.info('Deleting %s' % site_packages_filename) + os.unlink(site_packages_filename) + + return py_executable + + +def install_activate(home_dir, bin_dir, prompt=None): + if is_win or is_jython and os._name == 'nt': + files = { + 'activate.bat': ACTIVATE_BAT, + 'deactivate.bat': DEACTIVATE_BAT, + 'activate.ps1': ACTIVATE_PS, + } + + # MSYS needs paths of the form /c/path/to/file + drive, tail = os.path.splitdrive(home_dir.replace(os.sep, '/')) + home_dir_msys = (drive and "/%s%s" or "%s%s") % (drive[:1], tail) + + # Run-time conditional enables (basic) Cygwin compatibility + home_dir_sh = ("""$(if [ "$OSTYPE" "==" "cygwin" ]; then cygpath -u '%s'; else echo '%s'; fi;)""" % + (home_dir, home_dir_msys)) + files['activate'] = ACTIVATE_SH.replace('__VIRTUAL_ENV__', home_dir_sh) + + else: + files = {'activate': ACTIVATE_SH} + + # suppling activate.fish in addition to, not instead of, the + # bash script support. + files['activate.fish'] = ACTIVATE_FISH + + # same for csh/tcsh support... + files['activate.csh'] = ACTIVATE_CSH + + files['activate_this.py'] = ACTIVATE_THIS + + install_files(home_dir, bin_dir, prompt, files) + +def install_files(home_dir, bin_dir, prompt, files): + if hasattr(home_dir, 'decode'): + home_dir = home_dir.decode(sys.getfilesystemencoding()) + vname = os.path.basename(home_dir) + for name, content in files.items(): + content = content.replace('__VIRTUAL_PROMPT__', prompt or '') + content = content.replace('__VIRTUAL_WINPROMPT__', prompt or '(%s)' % vname) + content = content.replace('__VIRTUAL_ENV__', home_dir) + content = content.replace('__VIRTUAL_NAME__', vname) + content = content.replace('__BIN_NAME__', os.path.basename(bin_dir)) + writefile(os.path.join(bin_dir, name), content) + +def install_python_config(home_dir, bin_dir, prompt=None): + if sys.platform == 'win32' or is_jython and os._name == 'nt': + files = {} + else: + files = {'python-config': PYTHON_CONFIG} + install_files(home_dir, bin_dir, prompt, files) + for name, content in files.items(): + make_exe(os.path.join(bin_dir, name)) + +def install_distutils(home_dir): + distutils_path = change_prefix(distutils.__path__[0], home_dir) + mkdir(distutils_path) + ## FIXME: maybe this prefix setting should only be put in place if + ## there's a local distutils.cfg with a prefix setting? + home_dir = os.path.abspath(home_dir) + ## FIXME: this is breaking things, removing for now: + #distutils_cfg = DISTUTILS_CFG + "\n[install]\nprefix=%s\n" % home_dir + writefile(os.path.join(distutils_path, '__init__.py'), DISTUTILS_INIT) + writefile(os.path.join(distutils_path, 'distutils.cfg'), DISTUTILS_CFG, overwrite=False) + +def fix_local_scheme(home_dir, symlink=True): + """ + Platforms that use the "posix_local" install scheme (like Ubuntu with + Python 2.7) need to be given an additional "local" location, sigh. + """ + try: + import sysconfig + except ImportError: + pass + else: + if sysconfig._get_default_scheme() == 'posix_local': + local_path = os.path.join(home_dir, 'local') + if not os.path.exists(local_path): + os.mkdir(local_path) + for subdir_name in os.listdir(home_dir): + if subdir_name == 'local': + continue + copyfile(os.path.abspath(os.path.join(home_dir, subdir_name)), \ + os.path.join(local_path, subdir_name), symlink) + +def fix_lib64(lib_dir, symlink=True): + """ + Some platforms (particularly Gentoo on x64) put things in lib64/pythonX.Y + instead of lib/pythonX.Y. If this is such a platform we'll just create a + symlink so lib64 points to lib + """ + # PyPy's library path scheme is not affected by this. + # Return early or we will die on the following assert. + if is_pypy: + logger.debug('PyPy detected, skipping lib64 symlinking') + return + # Check we have a lib64 library path + if not [p for p in distutils.sysconfig.get_config_vars().values() + if isinstance(p, basestring) and 'lib64' in p]: + return + + logger.debug('This system uses lib64; symlinking lib64 to lib') + + assert os.path.basename(lib_dir) == 'python%s' % sys.version[:3], ( + "Unexpected python lib dir: %r" % lib_dir) + lib_parent = os.path.dirname(lib_dir) + top_level = os.path.dirname(lib_parent) + lib_dir = os.path.join(top_level, 'lib') + lib64_link = os.path.join(top_level, 'lib64') + assert os.path.basename(lib_parent) == 'lib', ( + "Unexpected parent dir: %r" % lib_parent) + if os.path.lexists(lib64_link): + return + if symlink: + os.symlink('lib', lib64_link) + else: + copyfile('lib', lib64_link) + +def resolve_interpreter(exe): + """ + If the executable given isn't an absolute path, search $PATH for the interpreter + """ + # If the "executable" is a version number, get the installed executable for + # that version + orig_exe = exe + python_versions = get_installed_pythons() + if exe in python_versions: + exe = python_versions[exe] + + if os.path.abspath(exe) != exe: + paths = os.environ.get('PATH', '').split(os.pathsep) + for path in paths: + if os.path.exists(join(path, exe)): + exe = join(path, exe) + break + if not os.path.exists(exe): + logger.fatal('The path %s (from --python=%s) does not exist' % (exe, orig_exe)) + raise SystemExit(3) + if not is_executable(exe): + logger.fatal('The path %s (from --python=%s) is not an executable file' % (exe, orig_exe)) + raise SystemExit(3) + return exe + +def is_executable(exe): + """Checks a file is executable""" + return os.path.isfile(exe) and os.access(exe, os.X_OK) + +############################################################ +## Relocating the environment: + +def make_environment_relocatable(home_dir): + """ + Makes the already-existing environment use relative paths, and takes out + the #!-based environment selection in scripts. + """ + home_dir, lib_dir, inc_dir, bin_dir = path_locations(home_dir) + activate_this = os.path.join(bin_dir, 'activate_this.py') + if not os.path.exists(activate_this): + logger.fatal( + 'The environment doesn\'t have a file %s -- please re-run virtualenv ' + 'on this environment to update it' % activate_this) + fixup_scripts(home_dir, bin_dir) + fixup_pth_and_egg_link(home_dir) + ## FIXME: need to fix up distutils.cfg + +OK_ABS_SCRIPTS = ['python', 'python%s' % sys.version[:3], + 'activate', 'activate.bat', 'activate_this.py', + 'activate.fish', 'activate.csh'] + +def fixup_scripts(home_dir, bin_dir): + if is_win: + new_shebang_args = ( + '%s /c' % os.path.normcase(os.environ.get('COMSPEC', 'cmd.exe')), + '', '.exe') + else: + new_shebang_args = ('/usr/bin/env', sys.version[:3], '') + + # This is what we expect at the top of scripts: + shebang = '#!%s' % os.path.normcase(os.path.join( + os.path.abspath(bin_dir), 'python%s' % new_shebang_args[2])) + # This is what we'll put: + new_shebang = '#!%s python%s%s' % new_shebang_args + + for filename in os.listdir(bin_dir): + filename = os.path.join(bin_dir, filename) + if not os.path.isfile(filename): + # ignore subdirs, e.g. .svn ones. + continue + lines = None + with open(filename, 'rb') as f: + try: + lines = f.read().decode('utf-8').splitlines() + except UnicodeDecodeError: + # This is probably a binary program instead + # of a script, so just ignore it. + continue + if not lines: + logger.warn('Script %s is an empty file' % filename) + continue + + old_shebang = lines[0].strip() + old_shebang = old_shebang[0:2] + os.path.normcase(old_shebang[2:]) + + if not old_shebang.startswith(shebang): + if os.path.basename(filename) in OK_ABS_SCRIPTS: + logger.debug('Cannot make script %s relative' % filename) + elif lines[0].strip() == new_shebang: + logger.info('Script %s has already been made relative' % filename) + else: + logger.warn('Script %s cannot be made relative (it\'s not a normal script that starts with %s)' + % (filename, shebang)) + continue + logger.notify('Making script %s relative' % filename) + script = relative_script([new_shebang] + lines[1:]) + with open(filename, 'wb') as f: + f.write('\n'.join(script).encode('utf-8')) + + +def relative_script(lines): + "Return a script that'll work in a relocatable environment." + activate = "import os; activate_this=os.path.join(os.path.dirname(os.path.realpath(__file__)), 'activate_this.py'); exec(compile(open(activate_this).read(), activate_this, 'exec'), dict(__file__=activate_this)); del os, activate_this" + # Find the last future statement in the script. If we insert the activation + # line before a future statement, Python will raise a SyntaxError. + activate_at = None + for idx, line in reversed(list(enumerate(lines))): + if line.split()[:3] == ['from', '__future__', 'import']: + activate_at = idx + 1 + break + if activate_at is None: + # Activate after the shebang. + activate_at = 1 + return lines[:activate_at] + ['', activate, ''] + lines[activate_at:] + +def fixup_pth_and_egg_link(home_dir, sys_path=None): + """Makes .pth and .egg-link files use relative paths""" + home_dir = os.path.normcase(os.path.abspath(home_dir)) + if sys_path is None: + sys_path = sys.path + for path in sys_path: + if not path: + path = '.' + if not os.path.isdir(path): + continue + path = os.path.normcase(os.path.abspath(path)) + if not path.startswith(home_dir): + logger.debug('Skipping system (non-environment) directory %s' % path) + continue + for filename in os.listdir(path): + filename = os.path.join(path, filename) + if filename.endswith('.pth'): + if not os.access(filename, os.W_OK): + logger.warn('Cannot write .pth file %s, skipping' % filename) + else: + fixup_pth_file(filename) + if filename.endswith('.egg-link'): + if not os.access(filename, os.W_OK): + logger.warn('Cannot write .egg-link file %s, skipping' % filename) + else: + fixup_egg_link(filename) + +def fixup_pth_file(filename): + lines = [] + prev_lines = [] + with open(filename) as f: + prev_lines = f.readlines() + for line in prev_lines: + line = line.strip() + if (not line or line.startswith('#') or line.startswith('import ') + or os.path.abspath(line) != line): + lines.append(line) + else: + new_value = make_relative_path(filename, line) + if line != new_value: + logger.debug('Rewriting path %s as %s (in %s)' % (line, new_value, filename)) + lines.append(new_value) + if lines == prev_lines: + logger.info('No changes to .pth file %s' % filename) + return + logger.notify('Making paths in .pth file %s relative' % filename) + with open(filename, 'w') as f: + f.write('\n'.join(lines) + '\n') + +def fixup_egg_link(filename): + with open(filename) as f: + link = f.readline().strip() + if os.path.abspath(link) != link: + logger.debug('Link in %s already relative' % filename) + return + new_link = make_relative_path(filename, link) + logger.notify('Rewriting link %s in %s as %s' % (link, filename, new_link)) + with open(filename, 'w') as f: + f.write(new_link) + +def make_relative_path(source, dest, dest_is_directory=True): + """ + Make a filename relative, where the filename is dest, and it is + being referred to from the filename source. + + >>> make_relative_path('/usr/share/something/a-file.pth', + ... '/usr/share/another-place/src/Directory') + '../another-place/src/Directory' + >>> make_relative_path('/usr/share/something/a-file.pth', + ... '/home/user/src/Directory') + '../../../home/user/src/Directory' + >>> make_relative_path('/usr/share/a-file.pth', '/usr/share/') + './' + """ + source = os.path.dirname(source) + if not dest_is_directory: + dest_filename = os.path.basename(dest) + dest = os.path.dirname(dest) + dest = os.path.normpath(os.path.abspath(dest)) + source = os.path.normpath(os.path.abspath(source)) + dest_parts = dest.strip(os.path.sep).split(os.path.sep) + source_parts = source.strip(os.path.sep).split(os.path.sep) + while dest_parts and source_parts and dest_parts[0] == source_parts[0]: + dest_parts.pop(0) + source_parts.pop(0) + full_parts = ['..']*len(source_parts) + dest_parts + if not dest_is_directory: + full_parts.append(dest_filename) + if not full_parts: + # Special case for the current directory (otherwise it'd be '') + return './' + return os.path.sep.join(full_parts) + + + +############################################################ +## Bootstrap script creation: + +def create_bootstrap_script(extra_text, python_version=''): + """ + Creates a bootstrap script, which is like this script but with + extend_parser, adjust_options, and after_install hooks. + + This returns a string that (written to disk of course) can be used + as a bootstrap script with your own customizations. The script + will be the standard virtualenv.py script, with your extra text + added (your extra text should be Python code). + + If you include these functions, they will be called: + + ``extend_parser(optparse_parser)``: + You can add or remove options from the parser here. + + ``adjust_options(options, args)``: + You can change options here, or change the args (if you accept + different kinds of arguments, be sure you modify ``args`` so it is + only ``[DEST_DIR]``). + + ``after_install(options, home_dir)``: + + After everything is installed, this function is called. This + is probably the function you are most likely to use. An + example would be:: + + def after_install(options, home_dir): + subprocess.call([join(home_dir, 'bin', 'easy_install'), + 'MyPackage']) + subprocess.call([join(home_dir, 'bin', 'my-package-script'), + 'setup', home_dir]) + + This example immediately installs a package, and runs a setup + script from that package. + + If you provide something like ``python_version='2.5'`` then the + script will start with ``#!/usr/bin/env python2.5`` instead of + ``#!/usr/bin/env python``. You can use this when the script must + be run with a particular Python version. + """ + filename = __file__ + if filename.endswith('.pyc'): + filename = filename[:-1] + with codecs.open(filename, 'r', encoding='utf-8') as f: + content = f.read() + py_exe = 'python%s' % python_version + content = (('#!/usr/bin/env %s\n' % py_exe) + + '## WARNING: This file is generated\n' + + content) + return content.replace('##EXT' 'END##', extra_text) + +##EXTEND## + +def convert(s): + b = base64.b64decode(s.encode('ascii')) + return zlib.decompress(b).decode('utf-8') + +##file site.py +SITE_PY = convert(""" +eJzFPf1z2zaWv/OvwMqToZTKdOJ0e3tO3RsncVrfuYm3yc7m1vXoKAmyWFMkS5C2tTd3f/u9DwAE ++CHb2+6cphNLJPDw8PC+8PAeOhqNTopCZkuxyZd1KoWScblYiyKu1kqs8lJU66Rc7hdxWW3h6eIm +vpZKVLlQWxVhqygInv/GT/BcfF4nyqAA3+K6yjdxlSziNN2KZFPkZSWXYlmXSXYtkiypkjhN/g4t +8iwSz387BsFZJmDmaSJLcStLBXCVyFfiYlut80yM6wLn/DL6Y/xqMhVqUSZFBQ1KjTNQZB1XQSbl +EtCElrUCUiaV3FeFXCSrZGEb3uV1uhRFGi+k+K//4qlR0zAMVL6Rd2tZSpEBMgBTAqwC8YCvSSkW ++VJGQryRixgH4OcNsQKGNsU1U0jGLBdpnl3DnDK5kErF5VaM53VFgAhlscwBpwQwqJI0De7y8kZN +YElpPe7gkYiZPfzJMHvAPHH8LucAjh+z4C9Zcj9l2MA9CK5aM9uUcpXcixjBwk95Lxcz/WycrMQy +Wa2ABlk1wSYBI6BEmswPClqOb/UKfXdAWFmujGEMiShzY35JPaLgrBJxqoBt6wJppAjzd3KexBlQ +I7uF4QAikDToG2eZqMqOQ7MTOQAocR0rkJKNEuNNnGTArD/GC0L7r0m2zO/UhCgAq6XEL7Wq3PmP +ewgArR0CTANcLLOadZYmNzLdTgCBz4B9KVWdVigQy6SUiyovE6kIAKC2FfIekJ6KuJSahMyZRm6n +RH+iSZLhwqKAocDjSyTJKrmuS5IwsUqAc4Er3n/8Sbw7fXN28kHzmAHGMnu9AZwBCi20gxMMIA5q +VR6kOQh0FJzjHxEvlyhk1zg+4NU0OHhwpYMxzL2I2n2cBQey68XVw8AcK1AmNFZA/f4bukzVGujz +Pw+sdxCcDFGFJs7f7tY5yGQWb6RYx8xfyBnBtxrOd1FRrV8DNyiEUwGpFC4OIpggPCCJS7NxnklR +AIulSSYnAVBoTm39VQRW+JBn+7TWLU4ACGWQwUvn2YRGzCRMtAvrNeoL03hLM9NNArvOm7wkxQH8 +ny1IF6VxdkM4KmIo/jaX10mWIULIC0G4F9LA6iYBTlxG4pxakV4wjUTI2otbokjUwEvIdMCT8j7e +FKmcsviibt2tRmgwWQmz1ilzHLSsSL3SqjVT7eW9w+hLi+sIzWpdSgBezz2hW+X5VMxBZxM2Rbxh +8arucuKcoEeeqBPyBLWEvvgdKHqiVL2R9iXyCmgWYqhgladpfgckOwoCIfawkTHKPnPCW3gH/wJc +/DeV1WIdBM5IFrAGhcgPgUIgYBJkprlaI+Fxm2bltpJJMtYUebmUJQ31OGIfMOKPbIxzDT7klTZq +PF1c5XyTVKiS5tpkJmzxsrBi/fia5w3TAMutiGamaUOnDU4vLdbxXBqXZC5XKAl6kV7bZYcxg54x +yRZXYsNWBt4BWWTCFqRfsaDSWVWSnACAwcIXZ0lRp9RIIYOJGAbaFAR/E6NJz7WzBOzNZjlAhcTm +ewH2B3D7O4jR3ToB+iwAAmgY1FKwfPOkKtFBaPRR4Bt905/HB049W2nbxEOu4iTVVj7OgjN6eFqW +JL4LWWCvqSaGghlmFbp21xnQEcV8NBoFgXGHtsp8zVVQldsjYAVhxpnN5nWChm82Q1Ovf6iARxHO +wF43287CAw1hOn0AKjldVmW+wdd2bp9AmcBY2CPYExekZSQ7yB4nvkbyuSq9ME3RdjvsLFAPBRc/ +nb4/+3L6SRyLy0alTdv67ArGPM1iYGuyCMBUrWEbXQYtUfElqPvEezDvxBRgz6g3ia+Mqxp4F1D/ +XNb0Gqax8F4Gpx9O3pyfzv7y6fSn2aezz6eAINgZGezRlNE81uAwqgiEA7hyqSJtX4NOD3rw5uST +fRDMEjX75mtgN3gyvpYVMHE5hhlPRbiJ7xUwaDilphPEsdMALHg4mYjvxOHz568OCVqxLbYADMyu +0xQfzrRFnyXZKg8n1PgXdumPWUlp/+3y6OsrcXwswl/i2zgMwIdqmjJL/Eji9HlbSOhawZ9xriZB +sJQrEL0biQI6fk5+8YQ7wJJAy1zb6V/yJDPvmSvdIUh/jKkH4DCbLdJYKWw8m4VABOrQ84EOETvX +KHVj6Fhs3a4TjQp+SgkLm2GXKf7Tg2I8p36IBqPodjGNQFw3i1hJbkXTh36zGeqs2WysBwRhJokB +h4vVUChME9RZZQJ+LXEe6rC5ylP8ifBRC5AA4tYKtSQukt46RbdxWks1diYFRByPW2RERZso4kdw +UcZgiZulm0za1DQ8A82AfGkOWrRsUQ4/e+DvgLoymzjc6PHei2mGmP477zQIB3A5Q1T3SrWgsHYU +F6cX4tWLw310Z2DPubTU8ZqjhU6yWtqHK1gtIw+MMPcy8uLSZYV6Fp8e7Ya5iezKdFlhpZe4lJv8 +Vi4BW2RgZ5XFT/QGduYwj0UMqwh6nfwBVqHGb4xxH8qzB2lB3wGotyEoZv3N0u9xMEBmChQRb6yJ +1HrXz6awKPPbBJ2N+Va/BFsJyhItpnFsAmfhPCZDkwgaArzgDCl1J0NQh2XNDivhjSDRXiwbxRoR +uHPU1Ff09SbL77IZ74SPUemOJ5Z1UbA082KDZgn2xHuwQoBkDhu7hmgMBVx+gbK1D8jD9GG6QFna +WwAgMPSKtmsOLLPVoynyrhGHRRiT14KEt5ToL9yaIWirZYjhQKK3kX1gtARCgslZBWdVg2YylDXT +DAZ2SOJz3XnEW1AfQIuKEZjNsYbGjQz9Lo9AOYtzVyk5/dAif/nyhdlGrSm+gojNcdLoQqzIWEbF +FgxrAjrBeGQcrSE2uAPnFsDUSrOm2P8k8oK9MVjPCy3b4AfA7q6qiqODg7u7u0hHF/Ly+kCtDv74 +p2+++dML1onLJfEPTMeRFh1qiw7oHXq00bfGAn1nVq7Fj0nmcyPBGkvyysgVRfy+r5NlLo72J1Z/ +Ihc3Zhr/Na4MKJCZGZSpDLQdNRg9U/vPoldqJJ6RdbZtxxP2S7RJtVbMt7rQo8rBEwC/ZZHXaKob +TlDiK7BusENfynl9HdrBPRtpfsBUUU7Hlgf2X14hBj5nGL4ypniGWoLYAi2+Q/qfmG1i8o60hkDy +oonq7J63/VrMEHf5eHm3vqYjNGaGiULuQInwmzxaAG3jruTgR7u2aPcc19Z8PENgLH1gmFc7lmMU +HMIF12LqSp3D1ejxgjTdsWoGBeOqRlDQ4CTOmdoaHNnIEEGid2M2+7ywugXQqRU5NPEBswrQwh2n +Y+3arOB4QsgDx+IlPZHgIh913r3gpa3TlAI6LR71qMKAvYVGO50DX44NgKkYlX8ZcUuzTfnYWhRe +gx5gOceAkMFWHWbCN64PONob9bBTx+oP9WYa94HARRpzLOpR0AnlYx6hVCBNxdjvOcTilrjdwXZa +HGIqs0wk0mpAuNrKo1eodhqmVZKh7nUWKVqkOXjFVisSIzXvfWeB9kH4uM+YaQnUZGjI4TQ6Jm/P +E8BQt8Pw2XWNgQY3DoMYbRJF1g3JtIZ/wK2g+AYFo4CWBM2CeayU+RP7HWTOzld/GWAPS2hkCLfp +kBvSsRgajnm/J5CMOhoDUpABCbvCSK4jq4MUOMxZIE+44bUclG6CESmQM8eCkJoB3Omlt8HBJxGe +gJCEIuT7SslCfCVGsHxtUX2c7v5dudQEIcZOA3IVdPTi2I1sOFGN41aUw2doP75BZyVFDhw8B5fH +DfS7bG6Y1gZdwFn3FbdFCjQyxWFGExfVK0MYN5j8h2OnRUMsM4hhKG8g70jHjDQJ7HJr0LDgBoy3 +5u2x9GM3YoF9x2GuDuXmHvZ/YZmoRa5Cipm0YxfuR3NFlzYW2/NkPoI/3gKMJlceJJnq+AVGWf6B +QUIPetgH3ZsshkWWcXmXZCEpME2/Y39pOnhYUnpG7uATbacOYKIY8Tx4X4KA0NHnAYgTagLYlctQ +abe/C3bnFEcWLncfeW7z5dGrqy5xp0MRHvvpX6rT+6qMFa5WyovGQoGr1TXgqHRhcnG21YeX+nAb +twllrmAXKT5++iKQEBzXvYu3T5t6w/CIzYNz8j4GddBrD5KrNTtiF0AEtSIyykH4dI58PLJPndyO +iT0ByJMYZseiGEiaT/4ROLsWCsbYX24zjKO1VQZ+4PU3X896IqMukt98PXpglBYx+sR+3PIE7cic +VLBrtqWMU3I1nD4UVMwa1rFtignrc9r+aR676vE5NVo29t3fAj8GCobUJfgIL6YN2bpTxY/vTg3C +03ZqB7DObtV89mgRYG+fz3+BHbLSQbXbOEnpXAEmv7+PytVs7jle0a89PEg7FYxDgr79l7p8AdwQ +cjRh0p2OdsZOTMC5ZxdsPkWsuqjs6RyC5gjMywtwjz+HFU6ve+B7Bge/r7p8IiBvTqMeMmpbbIZ4 +wQclhz1K9gnzfvqMf9dZP27mw4L1/zHLF/+cST5hKgaaNh4+rH5iuXbXAHuEeRpwO3e4hd2h+axy +ZZw7VklKPEfd9VzcUboCxVbxpAigLNnv64GDUqoPvd/WZclH16QCC1nu43HsVGCmlvH8ek3Mnjj4 +ICvExDZbUKzayevJ+4Qv1NFnO5Ow2Tf0c+c6NzErmd0mJfQFhTsOf/j442nYb0IwjgudHm9FHu83 +INwnMG6oiRM+pQ9T6Cld/nH10d66+AQ1GQEmIqzJ1iVsJxBs4gj9a/BARMg7sOVjdtyhL9ZycTOT +lDqAbIpdnaD4W3yNmNiMAj//S8UrSmKDmSzSGmnFjjdmH67qbEHnI5UE/0qnCmPqECUEcPhvlcbX +Ykydlxh60txI0anbuNTeZ1HmmJwq6mR5cJ0shfy1jlPc1svVCnDBwyv9KuLhKQIl3nFOAyctKrmo +y6TaAglileuzP0p/cBrOtzzRsYckH/MwATEh4kh8wmnjeybc0pDLBAf8Ew+cJO67sYOTrBDRc3if +5TMcdUY5vlNGqnsuT4+D9gg5ABgBUJj/aKIjd/4bSa/cA0Zac5eoqCU9UrqRhpycMYQynmCkg3/T +T58RXd4awPJ6GMvr3Vhet7G87sXy2sfyejeWrkjgwtqglZGEvsBV+1ijN9/GjTnxMKfxYs3tMPcT +czwBoijMBtvIFKdAe5EtPt8jIKS2nQNnetjkzyScVFrmHALXIJH78RBLb+ZN8rrTmbJxdGeeinFn +h3KI/L4HUUSpYnPqzvK2jKs48uTiOs3nILYW3WkDYCra6UQcK81uZ3OO7rYs1ejiPz//8PEDNkdQ +I5PeQN1wEdGw4FTGz+PyWnWlqdn8FcCO1NJPxKFuGuDeIyNrPMoe//OOMjyQccQdZSjkogAPgLK6 +bDM39ykMW891kpR+zkzOh03HYpRVo2ZSA0Q6ubh4d/L5ZEQhv9H/jlyBMbT1pcPFx7SwDbr+m9vc +Uhz7gFDr2FZj/Nw5ebRuOOJhG2vAdjzf1oPDxxjs3jCBP8t/KqVgSYBQkQ7+PoVQj945/Kb9UIc+ +hhE7yX/uyRo7K/adI3uOi+KIft+xQ3sA/7AT9xgzIIB2ocZmZ9DslVtK35rXHRR1gD7S1/vNe832 +1qu9k/EpaifR4wA6lLXNht0/75yGjZ6S1ZvT788+nJ+9uTj5/IPjAqIr9/HTwaE4/fGLoPwQNGDs +E8WYGlFhJhIYFrfQSSxz+K/GyM+yrjhIDL3enZ/rk5oNlrpg7jPanAiecxqThcZBM45C24c6/wgx +SvUGyakponQdqjnC/dKG61lUrvOjqVRpjs5qrbdeulbM1JTRuXYE0geNXVIwCE4xg1eUxV6ZXWHJ +J4C6zqoHKW2jbWJISkHBTrqAc/5lTle8QCl1hidNZ63oL0MX1/AqUkWawE7udWhlSXfD9JiGcfRD +e8DNePVpQKc7jKwb8qwHsUCr9Trkuen+k4bRfq0Bw4bB3sG8M0npIZSBjcltIsRGfJITynv4apde +r4GCBcODvgoX0TBdArOPYXMt1glsIIAn12B9cZ8AEFor4R8IHDnRAZljdkb4drPc/3OoCeK3/vnn +nuZVme7/TRSwCxKcShT2ENNt/A42PpGMxOnH95OQkaPUXPHnGssDwCGhAKgj7ZS/xCfos7GS6Urn +l/j6AF9oP4Fet7qXsih1937XOEQJeKbG5DU8U4Z+IaZ7WdhTnMqkBRorHyxmWEHopiGYz574tJZp +qvPdz96dn4LviMUYKEF87nYKw3G8BI/QdfIdVzi2QOEBO7wukY1LdGEpyWIZec16g9YoctTby8uw +60SB4W6vThS4jBPloj3GaTMsU04QISvDWphlZdZutUEKu22I4igzzBKzi5ISWH2eAF6mpzFviWCv +hKUeJgLPp8hJVpmMxTRZgB4FlQsKdQpCgsTFekbivDzjGHheKlMGBQ+LbZlcrys83YDOEZVgYPMf +T76cn32gsoTDV43X3cOcU9oJTDmJ5BhTBDHaAV/ctD/kqtmsj2f1K4SB2gf+tF9xdsoxD9Dpx4FF +/NN+xXVox85OkGcACqou2uKBGwCnW5/cNLLAuNp9MH7cFMAGMx8MxSKx7EUnerjz63KibdkyJRT3 +MS+fcICzKmxKmu7spqS1P3qOqwLPuZbj/kbwtk+2zGcOXW86b4aS39xPRwqxJBYw6rb2xzDZYZ2m +ejoOsw1xC21rtY39OXNipU67RYaiDEQcu50nLpP1K2HdnDnQS6PuABPfanSNJPaq8tHP2Uh7GB4m +ltidfYrpSGUsZAQwkiF17U8NPhRaBFAglP07diR3Onl+6M3RsQYPz1HrLrCNP4Ai1Lm4VOORl8CJ +8OVXdhz5FaGFevRIhI6nkskst3li+Llbo1f50p9jrwxQEBPFroyzazlmWFMD8yuf2AMhWNK2Hqkv +k6s+wyLOwDm9H+Dwrlz0H5wY1FqM0Gl3I7dtdeSTBxv0loLsJJgPvozvQPcXdTXmlRw4h+6tpRuG ++jBEzD6Epvr0fRxiOObXcGB9GsC91NCw0MP7deDsktfGOLLWPraqmkL7QnuwixK2ZpWiYxmnONH4 +otYLaAzucWPyR/apThSyv3vqxJyYkAXKg7sgvbmNdINWOGHE5UpcOZpQOnxTTaPfLeWtTMFogJEd +Y7XDL7baYRLZcEpvHthvxu5ie7Htx43eNJgdmXIMRIAKMXoDPbsQanDAFf5Z70Ti7Iac47d/PZuK +tx9+gn/fyI9gQbHmcSr+BqOLt3kJ20ou2qXbFLCAo+L9Yl4rLIwkaHRCwRdPoLd24ZEXT0N0ZYlf +UmIVpMBk2nLDt50AijxBKmRv3ANTLwG/TUFXywk1DmLfWoz0S6TBcI0L1oUc6JbRutqkaCac4Eiz +iJej87O3px8+nUbVPTK2+Tlygid+HhZORx8Nl3gMNhX2yaLGJ1eOv/yDTIsed1nvNU29DO41RQjb +kcLuL/kmjdjuKeISAwai2C7zRYQtgdO5RK+6A/954mwrH7TvnnFFWOOJPjxrnHh8DNQQP7f1zwga +Uh89J+pJCMVzrBXjx9Go3wJPBUW04c/zm7ulGxDXRT80wTamzazHfnerAtdMZw3PchLhdWyXwdSB +pkmsNvOFWx/4MRP6IhRQbnS8IVdxnVZCZrCVor093UgBCt4t6WMJYVZhK0Z1bhSdSe/irXJyj2Il +RjjqiIrq8RyGAoWw9f4xvmEzgLWGouYSaIBOiNK2KXe6qnqxZgnmnRBRryff4C7JXrnJL5rCPChv +jBeN/wrzRG+RMbqWlZ4/PxhPLl82CQ4UjF54Bb2LAoydyyZ7oDGL58+fj8S/Pez0MCpRmuc34I0B +7F5n5ZxeDxhsPTm7Wl2H3ryJgB8Xa3kJD64oaG6f1xlFJHd0pQWR9q+BEeLahJYZTfuWOeZYXcnn +y9yCz6m0wfhLltB1RxhRkqhs9a1RGG0y0kQsCYohjNUiSUKOTsB6bPMaa/Ewuqj5Rd4DxycIZopv +8WCMd9hrdCwpb9Zyj0XnWIwI8IhSyng0KmamajTAc3ax1WjOzrKkaspIXrhnpvoKgMreYqT5SsR3 +KBlmHi1iOGWdHqs2jnW+k0W9jUq+uHTjjK1Z8uuHcAfWBknLVyuDKTw0i7TIZbkw5hRXLFkklQPG +tEM43JkubyLrEwU9KI1AvZNVWFqJtm//YNfFxfQjHR/vm5F01lBlL8TimFCctfIKo6gZn6JPlpCW +b82XCYzygaLZ2hPwxhJ/0LFUrCHw7u1wyxnrTN/HwWkbzSUdAIfugLIK0rKjpyOci8csfGbagVs0 +8EM7c8LtNimrOk5n+tqHGfppM3uervG0ZXA7CzyttwK+fQ6O777O2AfHwSTXID0x49ZUZByLlY5M +RG5lmV+EVeTo5R2yrwQ+BVJmOTP10CZ2dGnZ1Raa6gRHR8UjqK9M8dKAQ26qZjoFJy7mU0pvMuUO +A86zn29JV1eI78T41VQctnY+i2KLNzkBss+Woe+KUTeYihMMMHNs34shvjsW45dT8ccd0KOBAY4O +3RHa+9gWhEEgr66eTMY0mRPZwr4U9of76hxG0PSM4+SqTf4umb4lKv1ri0pcIagTlV+2E5VbYw/u +WzsfH8lwA4pjlcjl/jOFJNRIN7p5mMEJPyyg37M5Wrp2vKmoocK5OWxG7ho96GhE4zbbQUxRulZf +XL+LuoYNp71zwKTJtFIV7S1zmMao0WsRFQDM+o7S8Bve7QLvNSlc/2zwiFUXAViwPREEXenJB2ZN +w0ZQH3QEn6QBHmAUEeJhaqMoXMl6goiEdA8OMdFXrUNsh+N/d+bhEoOho9AOlt98vQtPVzB7izp6 +FnR3pYUnsra8ollu8+kPzHmM0tf1NwmMA6URHXBWzVWV5GYeYfYy30GT2yzmDV4GSSfTaBJT6bpN +vJXmW7/Qj6HYASWTwVqAJ1Wv8CD5lu62PFGU9IZX1Hx9+HJqKoMZkJ7Aq+jVV/oKSOpmLj/wfeyp +3rvBS93vMPoXB1hS+b3tq85uhqZ13LoLyh8spOjZJJpZOjSG6eE6kGbNYoF3JjbEZN/aXgDyHryd +Ofg55vLTHBw22JBGfei6GqOR3iHVNiDAD5uMIcl5VNdGkSLSu4RtSHnuUpxPFgXdq9+CYAgBOX8d +8xt0BeviyIbYjE3Bk8+xm82Jn+qmt+6M7Qka2+om3DV97r9r7rpFYGdukhk6c/frS10a6L7DVrSP +Bhze0IR4VIlEo/H7jYlrB6Y6h6Y/Qq8/SH63E850wKw8BMZk7GC8n9hTY2/M/iZeuN8xIWyfL2R2 +y4l7nY3WtDs2o83xj/EUOPkFn9sbBiijaak5kPdLdMPejHNkZ/L6Ws1ivN1xRptsyufq7J7Mtu09 +Xc4nY7U1uy28tAhAGG7Smbducj0wBuhKvmWa06Gc22kEDU1Jw04WskqWbBL01g7ARRwxpf4mEM9p +xKNUYqBb1WVRwm54pO8i5jydvtTmBqgJ4G1idWNQNz2m+mpaUqyUHGZKkDlO20ryASKwEe+YhtnM +vgNeedFcs5BMLTPIrN7IMq6aK4b8jIAENl3NCFR0jovrhOcaqWxxiYtYYnnDQQoDZPb7V7Cx9DbV +O+5VmFht93h2oh465PuUKxscY2S4OLm31wu611ot6Wpr1zu0zRqus1cqwTKYu/JIR+pYGb/V93fx +HbMcyUf/0uEfkHe38tLPQrfqjL1bi4bzzFUI3Qub8MYAMs599zB2OKB742JrA2zH9/WFZZSOhznQ +2FJR++S9CqcZbdJEkDBh9IEIkl8U8MQIkgf/kREkfWsmGBqNj9YDvWUCD4SaWD24V1A2jAB9ZkAk +PMBuXWBoTOXYTbovcpXcj+yF0qwrnUo+Yx6QI7t3kxEIvmpSuRnK3lVwuyJIvnTR4+/PP745OSda +zC5O3v7HyfeUlIXHJS1b9egQW5bvM7X3vfRvN9ymE2n6Bm+w7bkhlmuYNITO+04OQg+E/nq1vgVt +KzL39VCHTt1PtxMgvnvaLahDKrsXcscv0zUmbvpMK0870E85qdb8cjITzCNzUsfi0JzEmffN4YmW +0U5seWjhnPTWrjrR/qq+BXQg7j2xSda0Anhmgvxlj0xMxYwNzLOD0v7ffFBmOFYbmht0QAoX0rnJ +kS5xZFCV//8TKUHZxbi3Y0dxau/mpnZ8PKTspfN49ruQkSGIV+436s7PFfalTAeoEASs8PQ9hYyI +0X/6QNWmHzxT4nKfCov3Udlc2V+4Ztq5/WuCSQaVve9LcYISH7NC41WduokDtk+nAzl9dBqVr5xK +FtB8B0DnRjwVsDf6S6wQ51sRwsZRu2SYHEt01Jf1Ocij3XSwN7R6IfaHyk7dskshXg43XLYqO3WP +Q+6hHuihalPc51hgzNIcqicV3xFkPs4UdMGX53zgGbre9sPX28uXR/ZwAfkdXzuKhLLJRo5hv3Sy +MXdeKul0J2Ypp5Suh3s1JySsW1w5UNknGNrbdEpSBvY/Js+BIY289/0hM9PDu3p/1MbUst4RTEmM +n6kJTcsp4tG42yeT7nQbtdUFwgVJjwDSUYEAC8F0dKOTILrlLO/xC70bnNd0Ha97whQ6UkHJYj5H +cA/j+zX4tbtTIfGjujOKpj83aHOgXnIQbvYduNXEC4UMm4T21Bs+GHABuCa7v//LR/TvpjHa7oe7 +/Grb6lVvHSD7spj5iplBLRKZxxEYGdCbY9LWWC5hBB2voWno6DJUMzfkC3T8KJsWL9umDQY5szPt +AVijEPwfucjncQ== +""") + +##file activate.sh +ACTIVATE_SH = convert(""" +eJytVd9v2kAMfs9fYQLq2m4MscdNVKMqEkgtVIQxbeuUHolpTgsXdHehpT/+9/mSEBJS2MOaB0ji +z77P9menDpOAK5jzEGERKw0zhFihD/dcB2CrKJYewoyLFvM0XzGNNpzOZbSAGVPBqVWHdRSDx4SI +NMhYANfgc4meDteW5ePGC45P4MkCumKhUENzDsu1H3lw1vJx1RJxGMKns6O2lWDqINGgotAHFCsu +I7FAoWHFJGezEFWGqsEvaD5C42naHb93X+A3+elYCgVaxgh8DmQAys9HL2SS0mIaWBgm7mTN/O3G +kzu6vHCng/HkW/fSve5O+hTOpnhfQAcoEry5jKVjNypoO0fgwzKSOgHm79KUK06Jfc7/RebHpD8a +9kdXvT2UcnuFWG6p0stNB0mWUUQ1q3uiGRVEMfXHR03dTuQATPjwqIIPcB9wL4CArRAY/ZHJixYL +Y9YBtcAoLQtFevOoI9QaHcEdMSAB0d08kuZhyUiSmav6CPCdVBnFOjNrLu6yMCWgKRA0TInBC5i4 +QwX3JG/mm581GKnSsSSxJTFHf9MAKr8w5T/vOv1mUurn5/zlT6fvTntjZzAaNl9rQ5JkU5KIc0GX +inagwU57T2eddqWlTrvaS6d9sImZeUMkhWysveF0m37NcGub9Dpgi0j4qGiOzATjDr06OBjOYQOo +7RBoGtNm9Denv1i0LVI7lxJDXLHSSBeWRflsyyqw7diuW3h0XdvK6lBMyaoMG1UyHdTsoYBuue75 +YOgOu1c91/2cwYpznPPeDoQpGL2xSm09NKp7BsvQ2hnT3aMs07lUnskpxewvBk73/LLnXo9HV9eT +ijB3hWBO2ygoiWg/bKuZxqCCQq0DD3vkWIVvI2KosIw+vqW1gIItEG5KJb+xb09g65ktwYKgTc51 +uGJ/EFQs0ayEWLCQM5V9N4g+1+8UbXOJzF8bqhKtIqIwicWvzNFROZJlpfD8A7Vc044R0FxkcezG +VzsV75usvTdYef+57v5n1b225qhXfwEmxHEs +""") + +##file activate.fish +ACTIVATE_FISH = convert(""" +eJyFVVFv2zYQftevuMoOnBS1gr0WGIZ08RADSRw4boBhGGhGOsUcKFIjKbUu9uN7lC2JsrXWDzZM +fnf38e6+uwlsdsJCLiRCUVkHrwiVxYy+hHqDbQKvQl3z1ImaO0xyYXdbeP9FuJ1QwMFUSnmcP4dL +2DlXfry+9v/sDqVMUl3AFVi0Vmj1PokmcKtBaecNQTjIhMHUyX0SRXmlKIpWkGEbDuYZzBZfCVcL +4youUdVQ6AyBqwwMusoocBrcDsmpKbgEQgijVYHKJbMI6DMhoEUHWmbhLdTcCP4q0TYokYNDev5c +QTxlq/tb9rJcbz7f3LOnm81d3GD8x3uav30FfwrnwCEOYRyAKot+FvXPzd3q8W71sBiJ3d2dMugu +fsxjCPsBmz+Wz3fsab16eNqw1ctivV7eBnwm8EzeuQIsSrcHqVMqwHbqq8/aarKSO+oYKhKXUn9p +SmWw0DVBdQ7bBlwaTR62bc+1tpaYb5PhUyScu48CRgvDLQbtMrMnMQ6dY5022JDRRrwJxWUfJwwP +ge0YIAVGfcUC1M8s8MxitFZjmR9W64hui7p4fBlWMZ5y81b/9cvfMbz7FWZKq4yOTeW1hbNBEWU+ +b+/ejXMu95lOx696uXb8Go4T+Kw8R2EMSqx5KLkkCkQ+ZBZFbZsHL4OYseAvY3EPO5MYTBuhDZQa +TwPza8Y+LR/Z483Dgjwd4R3f7bTXx9Znkw6T6PAL83/hRD3jNAKFjuEx9NJkq5t+fabLvdvRwbw4 +nEFTzwO6U+q34cvY7fL55tP94tg58XEA/q7LfdPsaUXFoEIMJdHF5iSW0+48CnDQ82G7n3XzAD6q +Bmo5XuOA0NQ67ir7AXJtQhtLKO7XhC0l39PGOBsHPvzBuHUSjoOnA0ldozGC9gZ5rek3+y3ALHO/ +kT7AP379lQZLSnFDLtwWihfYxw4nZd+ZR7myfkI2ZTRCuRxmF/bCzkbhcElvYamW9PbDGrvqPKC0 ++D/uLi/sFcxGjOHylYagZzzsjjhw206RQwrWIwOxS2dnk+40xOjX8bTPegz/gdWVSXuaowNuOLda +wYyNuRPSTcd/B48Ppeg= +""") + +##file activate.csh +ACTIVATE_CSH = convert(""" +eJx1U2FP2zAQ/e5f8TAV3Soo+0zXbYUiDQkKQgVp2ibjJNfFUuIg22nVf885SVFLO3+I7Lt3fr6X +d8eY58ZjYQpCWfuAhFB7yrAyIYf0Ve1SQmLsuU6DWepAw9TnEoOFq0rwdjAUx/hV1Ui1tVWAqy1M +QGYcpaFYx+yVI67LkKwx1UuTEaYGl4X2Bl+zJpAlP/6V2hTDtCq/DYXQhdEeGW040Q/Eb+t9V/e3 +U/V88zh/mtyqh8n8J47G+IKTE3gKZJdoYrK3h5MRU1tGYS83gqNc+3yEgyyP93cP820evHLvr2H8 +kaYB/peoyY7aVHzpJnE9e+6I5Z+ji4GMTNJWNuOQq6MA1N25p8pW9HWdVWlfsNpPDbdxjgpaahuw +1M7opCA/FFu1uwxC7L8KUqmto1KyQe3rx0I0Eovdf7BVe67U5c1MzSZ310pddGheZoFPWyytRkzU +aCA/I+RkBXhFXr5aWV0SxjhUI6jwdAj8kmhPzX7nTfJFkM3MImp2VdVFFq1vLHSU5szYQK4Ri+Jd +xlW2JBtOGcyYVW7SnB3v6RS91g3gKapZ0oWxbHVteYIIq3iv7QeuSrUj6KSqQ+yqsxDj1ivNQxKF +YON10Q+NH/ARS95i5Tuqq2Vxfvc23f/FO6zrtXXmJr+ZtMY9/A15ZXFWtmch2rEQ4g1ryVHH +""") + +##file activate.bat +ACTIVATE_BAT = convert(""" +eJx9Ul9LhEAQfxf8DoOclI/dYyFkaCmcq4gZQTBUrincuZFbff12T133TM+nnd35/Zvxlr7XDFhV +mUZHOVhFlOWP3g4DUriIWoVomYZpNBWUtGpaWgImO191pFkSpzlcmgaI70jVX7n2Qp8tuByg+46O +CMHbMq64T+nmlJt082D1T44muCDk2prgEHF4mdI9RaS/QwSt3zSyIAaftRccvqVTBziD1x/WlPD5 +xd729NDBb8Nr4DU9QNMKsJeH9pkhPedhQsIkDuCDCa6A+NF9IevVFAohkqizdHetg/tkWvPoftWJ +MCqnOxv7/x7Np6yv9P2Ker5dmX8yNyCkkWnbZy3N5LarczlqL8htx2EM9rQ/2H5BvIsIEi8OEG8U ++g8CsNTr +""") + +##file deactivate.bat +DEACTIVATE_BAT = convert(""" +eJyFkN0KgkAUhO8F32EQpHqFQEjQUPAPMaErqVxzId3IrV6/XST/UDx3c86c4WMO5FYysKJQFVVp +CEfqxsnJ9DI7SA25i20fFqs3HO+GYLsDZ7h8GM3xfLHrg1QNvpSX4CWpQGvokZk4uqrQAjXjyElB +a5IjCz0r+2dHcehHCe5MZNmB5R7TdqMqECMptHZh6DN/utb7Zs6Cej8OXYE5J04YOKFvD4GkHuJ0 +pilSd1jG6n87tDZ+BUwUOepI6CGSkFMYWf0ihvT33Qj1A+tCkSI= +""") + +##file activate.ps1 +ACTIVATE_PS = convert(""" +eJylWdmO41hyfW+g/0FTU7C7IXeJIqmtB/3AnZRIStxF2kaBm7gv4ipyMF/mB3+Sf8GXVGVl1tLT +43ECSqR4b5wbETeWE8z/+a///vNCDaN6cYtSf5G1dbNw/IVXNIu6aCvX9xa3qsgWl0IJ/7IYinbh +2nkOVqs2X0TNjz/8eeFFle826fBhQRaLBkD9uviw+LCy3Sbq7Mb/UNbrH3+YNtLcVaB+Xbipb+eL +tly0eVsD/M6u6g8//vC+dquobH5VWU75eMFUdvHb4n02RHlXuHYTFfmHbHCLLLNz70NpN+GrBI4p +1EeSk4FAXaZR88u0vPip8usi7fznt3fvP+OuPnx49/Pil4td+XnzigIAPoqYQH2J8v4z+C+8b98m +Q25t7k76LIK0cOz0V89/MXXx0+Lf6z5q3PA/F+/FIif9uqnaadFf/PzXSXYBfqIb2NeApecJwPzI +dlL/149nnvyoc7KqYfzTAT8v/voUmX7e+3n364tffl/oVaDyswKY/7J18e6bve8Wv9RuUfqfLHmK +/u139Hwx+9ePRep97KKqae30YwmCo2y+0vTz1k+rv7159B3pb1SOGj97Pe8/flfkC1Vn/7xYR4n6 +lypNEGDDV5f7lcjil3S+4++p881Wv6qKyn5GQg1yJwcp4BZ5E+Wt/z1P/umbiHir4J8Xip/eFt6n +9T/9gU9eY+7zUX97Jlmb136ziKrKT/3OzpvP8VX/+MObSP0lL3LvVZlJ9v1b8357jXyw8rXxYPXN +11n4UzJ8G8S/vUbuJ6RPj999DbtS5kys//JusXwrNLnvT99cFlBNwXCe+niRz8JF/ezNr9Pze+H6 +18W7d5PPvozW7+387Zto/v4pL8BvbxTzvIW9KCv/Fj0WzVQb/YXbVlPZWTz3/9vCaRtQbPN/Bb+j +2rUrDxTVD68gfQXu/ZewAFX53U/vf/rD2P3558W7+W79Po1y/xXoX/6RFHyNIoVjgAG4H0RTcAe5 +3bSVv3DSwk2mZYHjFB8zj6fC4sLOFTHJJQrwzFYJgso0ApOoBzFiRzzQKjIQCCbQMIFJGCKqGUyS +8AkjiF2wTwmMEbcEUvq8Nj+X0f4YcCQmYRiOY7eRbAJDqzm1chOoNstbJ8oTBhZQ2NcfgaB6QjLp +U4+SWFjQGCZpyqby8V4JkPGs9eH1BscXIrTG24QxXLIgCLYNsIlxSYLA6SjAeg7HAg4/kpiIB8k9 +TCLm0EM4gKIxEj8IUj2dQeqSxEwYVH88qiRlCLjEYGuNIkJB1BA5dHOZdGAoUFk54WOqEojkuf4Q +Ig3WY+96TDlKLicMC04h0+gDCdYHj0kz2xBDj9ECDU5zJ0tba6RKgXBneewhBG/xJ5m5FX+WSzsn +wnHvKhcOciw9NunZ0BUF0n0IJAcJMdcLqgQb0zP19dl8t9PzmMBjkuIF7KkvHgqEovUPOsY0PBB1 +HCtUUhch83qEJPjQcNQDsgj0cRqx2ZbnnlrlUjE1EX2wFJyyDa/0GLrmKDEFepdWlsbmVU45Wiwt +eFM6mfs4kxg8yc4YmKDy67dniLV5FUeO5AKNPZaOQQ++gh+dXE7dbJ1aTDr7S4WPd8sQoQkDyODg +XnEu/voeKRAXZxB/e2xaJ4LTFLPYEJ15Ltb87I45l+P6OGFA5F5Ix8A4ORV6M1NH1uMuZMnmFtLi +VpYed+gSq9JDBoHc05J4OhKetrk1p0LYiKipxLMe3tYS7c5V7O1KcPU8BJGdLfcswhoFCSGQqJ8f +ThyQKy5EWFtHVuNhvTnkeTc8JMpN5li3buURh0+3ZGuzdwM55kon+8urbintjdQJf9U1D0ah+hNh +i1XNu4fSKbTC5AikGEaj0CYM1dpuli7EoqUt7929f1plxGGNZnixFSFP2qzhlZMonu2bB9OWSqYx +VuHKWNGJI8kqUhMTRtk0vJ5ycZ60JlodlmN3D9XiEj/cG2lSt+WV3OtMgt1Tf4/Z+1BaCus740kx +Nvj78+jMd9tq537Xz/mNFyiHb0HdwHytJ3uQUzKkYhK7wjGtx3oKX43YeYoJVtqDSrCnQFzMemCS +2bPSvP+M4yZFi/iZhAjL4UOeMfa7Ex8HKBqw4umOCPh+imOP6yVTwG2MplB+wtg97olEtykNZ6wg +FJBNXSTJ3g0CCTEEMdUjjcaBDjhJ9fyINXgQVHhA0bjk9lhhhhOGzcqQSxYdj3iIN2xGEOODx4qj +Q2xikJudC1ujCVOtiRwhga5nPdhe1gSa649bLJ0wCuLMcEYIeSy25YcDQHJb95nfowv3rQnin0fE +zIXFkM/EwSGxvCCMgEPNcDp/wph1gMEa8Xd1qAWOwWZ/KhjlqzgisBpDDDXz9Cmov46GYBKHC4zZ +84HJnXoTxyWNBbXV4LK/r+OEwSN45zBp7Cub3gIYIvYlxon5BzDgtPUYfXAMPbENGrI+YVGSeTQ5 +i8NMB5UCcC+YRGIBhgs0xhAGwSgYwywpbu4vpCSTdEKrsy8osXMUnHQYenQHbOBofLCNNTg3CRRj +A1nXY2MZcjnXI+oQ2Zk+561H4CqoW61tbPKv65Y7fqc3TDUF9CA3F3gM0e0JQ0TPADJFJXVzphpr +2FzwAY8apGCju1QGOiUVO5KV6/hKbtgVN6hRVwpRYtu+/OC6w2bCcGzZQ8NCc4WejNEjFxOIgR3o +QqR1ZK0IaUxZ9nbL7GWJIjxBARUhAMnYrq/S0tVOjzlOSYRqeIZxaSaOBX5HSR3MFekOXVdUPbjX +nru61fDwI8HRYPUS7a6Inzq9JLjokU6P6OzT4UCH+Nha+JrU4VqEo4rRHQJhVuulAnvFhYz5NWFT +aS/bKxW6J3e46y4PLagGrCDKcq5B9EmP+s1QMCaxHNeM7deGEV3WPn3CeKjndlygdPyoIcNaL3dd +bdqPs47frcZ3aNWQ2Tk+rjFR01Ul4XnQQB6CSKA+cZusD0CP3F2Ph0e78baybgioepG12luSpFXi +bHbI6rGLDsGEodMObDG7uyxfCeU+1OiyXYk8fnGu0SpbpRoEuWdSUlNi5bd9nBxYqZGrq7Qa7zV+ +VLazLcelzzP9+n6+xUtWx9OVJZW3gk92XGGkstTJ/LreFVFF2feLpXGGuQqq6/1QbWPyhJXIXIMs +7ySVlzMYqoPmnmrobbeauMIxrCr3sM+qs5HpwmmFt7SM3aRNQWpCrmeAXY28EJ9uc966urGKBL9H +18MtDE5OX97GDOHxam11y5LCAzcwtkUu8wqWI1dWgHyxGZdY8mC3lXzbzncLZ2bIUxTD2yW7l9eY +gBUo7uj02ZI3ydUViL7oAVFag37JsjYG8o4Csc5R7SeONGF8yZP+7xxi9scnHvHPcogJ44VH/LMc +Yu6Vn3jEzCFw9Eqq1ENQAW8aqbUwSiAqi+nZ+OkZJKpBL66Bj8z+ATqb/8qDIJUeNRTwrI0YrVmb +9FArKVEbCWUNSi8ipfVv+STgkpSsUhcBg541eeKLoBpLGaiHTNoK0r4nn3tZqrcIULtq20Df+FVQ +Sa0MnWxTugMuzD410sQygF4qdntbswiJMqjs014Irz/tm+pd5oygJ0fcdNbMg165Pqi7EkYGAXcB +dwxioCDA3+BY9+JjuOmJu/xyX2GJtaKSQcOZxyqFzTaa6/ot21sez0BtKjirROKRm2zuai02L0N+ +ULaX8H5P6VwsGPbYOY7sAy5FHBROMrMzFVPYhFHZ7M3ZCZa2hsT4jGow6TGtG8Nje9405uMUjdF4 +PtKQjw6yZOmPUmO8LjFWS4aPCfE011N+l3EdYq09O3iQJ9a01B3KXiMF1WmtZ+l1gmyJ/ibAHZil +vQzdOl6g9PoSJ4TM4ghTnTndEVMOmsSSu+SCVlGCOLQRaw9oLzamSWP62VuxPZ77mZYdfTRGuNBi +KyhZL32S2YckO/tU7y4Bf+QKKibQSKCTDWPUwWaE8yCBeL5FjpbQuAlb53mGX1jptLeRotREbx96 +gnicYz0496dYauCjpTCA4VA0cdLJewzRmZeTwuXWD0talJsSF9J1Pe72nkaHSpULgNeK1+o+9yi0 +YpYwXZyvaZatK2eL0U0ZY6ekZkFPdC8JTF4Yo1ytawNfepqUKEhwznp6HO6+2l7L2R9Q3N49JMIe +Z+ax1mVaWussz98QbNTRPo1xu4W33LJpd9H14dd66ype7UktfEDi3oUTccJ4nODjwBKFxS7lYWiq +XoHu/b7ZVcK5TbRD0F/2GShg2ywwUl07k4LLqhofKxFBNd1grWY+Zt/cPtacBpV9ys2z1moMLrT3 +W0Elrjtt5y/dvDQYtObYS97pqj0eqmwvD3jCPRqamGthLiF0XkgB6IdHLBBwDGPiIDh7oPaRmTrN +tYA/yQKFxRiok+jM6ciJq/ZgiOi5+W4DEmufPEubeSuYJaM3/JHEevM08yJAXUQwb9LS2+8FOfds +FfOe3Bel6EDSjIEIKs4o9tyt67L1ylQlzhe0Q+7ue/bJnWMcD3q6wDSIQi8ThnRM65aqLWesi/ZM +xhHmQvfKBbWcC194IPjbBLYR9JTPITbzwRcu+OSFHDHNSYCLt29sAHO6Gf0h/2UO9Xwvhrjhczyx +Ygz6CqP4IwxQj5694Q1Pe2IR+KF/yy+5PvCL/vgwv5mPp9n4kx7fnY/nmV++410qF/ZVCMyv5nAP +pkeOSce53yJ6ahF4aMJi52by1HcCj9mDT5i+7TF6RoPaLL+cN1hXem2DmX/mdIbeeqwQOLD5lKO/ +6FM4x77w6D5wMx3g0IAfa2D/pgY9a7bFQbinLDPz5dZi9ATIrd0cB5xfC0BfCCZO7TKP0jQ2Meih +nRXhkA3smTAnDN9IW2vA++lsgNuZ2QP0UhqyjUPrDmgfWP2bWWiKA+YiEK7xou8cY0+d3/bk0oHR +QLrq4KzDYF/ljQDmNhBHtkVNuoDey6TTeaD3SHO/Bf4d3IwGdqQp6FuhmwFbmbQBssDXVKDBYOpk +Jy7wxOaSRwr0rDmGbsFdCM+7XU/84JPu3D/gW7QXgzlvbjixn99/8CpWFUQWHFEz/RyXvzNXTTOd +OXLNNFc957Jn/YikNzEpUdRNxXcC6b76ccTwMGoKj5X7c7TvHFgc3Tf4892+5A+iR+D8OaaE6ACe +gdgHcyCoPm/xiDCWP+OZRjpzfj5/2u0i4qQfmIEOsTV9Hw6jZ3Agnh6hiwjDtGYxWvt5TiWEuabN +77YCyRXwO8P8wdzG/8489KwfFBZWI6Vvx76gmlOc03JI1HEfXYZEL4sNFQ3+bqf7e2hdSWQknwKF +ICJjGyDs3fdmnnxubKXebpQYLjPgEt9GTzKkUgTvOoQa1J7N3nv4sR6uvYFLhkXZ+pbCoU3K9bfq +gF7W82tNutRRZExad+k4GYYsCfmEbvizS4jsRr3fdzqjEthpEwm7pmN7OgVzRbrktjrFw1lc0vM8 +V7dyTJ71qlsd7v3KhmHzeJB35pqEOk2pEe5uPeCToNkmedmxcKbIj+MZzjFSsvCmimaMQB1uJJKa ++hoWUi7aEFLvIxKxJavqpggXBIk2hr0608dIgnfG5ZEprqmH0b0YSy6jVXTCuIB+WER4d5BPVy9Q +M4taX0RIlDYxQ2CjBuq78AAcHQf5qoKP8BXHnDnd/+ed5fS+csL4g3eWqECaL+8suy9r8hx7c+4L +EegEWdqAWN1w1NezP34xsxLkvRRI0DRzKOg0U+BKfQY128YlYsbwSczEg2LqKxRmcgiwHdhc9MQJ +IwKQHlgBejWeMGDYYxTOQUiJOmIjJbzIzHH6lAMP+y/fR0v1g4wx4St8fcqTt3gz5wc+xXFZZ3qI +JpXI5iJk7xmNL2tYsDpcqu0375Snd5EKsIvg8u5szTOyZ4v06Ny2TZXRpHUSinh4IFp8Eoi7GINJ +02lPJnS/9jSxolJwp2slPMIEbjleWw3eec4XaetyEnSSqTPRZ9fVA0cPXMqzrPYQQyrRux3LaAh1 +wujbgcObg1nt4iiJ5IMbc/WNPc280I2T4nTkdwG8H6iS5xO2WfsFsruBwf2QkgZlb6w7om2G65Lr +r2Gl4dk63F8rCEHoUJ3fW+pU2Srjlmcbp+JXY3DMifEI22HcHAvT7zzXiMTr7VbUR5a2lZtJkk4k +1heZZFdru8ucCWMTr3Z4eNnjLm7LW7rcN7QjMpxrsCzjxndeyFUX7deIs3PQkgyH8k6luI0uUyLr +va47TBjM4JmNHFzGPcP6BV6cYgQy8VQYZe5GmzZHMxyBYhGiUdekZQ/qwyxC3WGylQGdUpSf9ZCP +a7qPdJd31fPRC0TOgzupO7nLuBGr2A02yuUQwt2KQG31sW8Gd9tQiHq+hPDt4OzJuY4pS8XRsepY +tsd7dVEfJFmc15IYqwHverrpWyS1rFZibDPW1hUUb+85CGUzSBSTK8hpvee/ZxonW51TUXekMy3L +uy25tMTg4mqbSLQQJ+skiQu2toIfBFYrOWql+EQipgfT15P1aq6FDK3xgSjIGWde0BPftYchDTdM +i4QdudHFkN0u6fSKiT09QLv2mtSblt5nNzBR6UReePNs+khE4rHcXuoK21igUKHl1c3MXMgPu7y8 +rKQDxR6N/rffXv+lROXet/9Q+l9I4D1U +""") + +##file distutils-init.py +DISTUTILS_INIT = convert(""" +eJytV1uL4zYUfvevOE0ottuMW9q3gVDa3aUMXXbLMlDKMBiNrSTqOJKRlMxkf33PkXyRbGe7Dw2E +UXTu37lpxLFV2oIyifAncxmOL0xLIfcG+gv80x9VW6maw7o/CANSWWBwFtqeWMPlGY6qPjV8A0bB +C4eKSTgZ5LRgFeyErMEeOBhbN+Ipgeizhjtnhkn7DdyjuNLPoCS0l/ayQTG0djwZC08cLXozeMss +aG5EzQ0IScpnWtHSTXuxByV/QCmxE7y+eS0uxWeoheaVVfqSJHiU7Mhhi6gULbOHorshkrEnKxpT +0n3A8Y8SMpuwZx6aoix3ouFlmW8gHRSkeSJ2g7hU+kiHLDaQw3bmRDaTGfTnty7gPm0FHbIBg9U9 +oh1kZzAFLaue2R6htPCtAda2nGlDSUJ4PZBgCJBGVcwKTAMz/vJiLD+Oin5Z5QlvDPdulC6EsiyE +NFzb7McNTKJzbJqzphx92VKRFY1idenzmq3K0emRcbWBD0ryqc4NZGmKOOOX9Pz5x+/l27tP797c +f/z0d+4NruGNai8uAM0bfsYaw8itFk8ny41jsfpyO+BWlpqfhcG4yxLdi/0tQqoT4a8Vby382mt8 +p7XSo7aWGdPBc+b6utaBmCQ7rQKQoWtAuthQCiold2KfJIPTT8xwg9blPumc+YDZC/wYGdAyHpJk +vUbHbHWAp5No6pK/WhhLEWrFjUwtPEv1Agf8YmnsuXUQYkeZoHm8ogP16gt2uHoxcEMdf2C6pmbw +hUMsWGhanboh4IzzmsIpWs134jVPqD/c74bZHdY69UKKSn/+KfVhxLgUlToemayLMYQOqfEC61bh +cbhwaqoGUzIyZRFHPmau5juaWqwRn3mpWmoEA5nhzS5gog/5jbcFQqOZvmBasZtwYlG93k5GEiyw +buHhMWLjDarEGpMGB2LFs5nIJkhp/nUmZneFaRth++lieJtHepIvKgx6PJqIlD9X2j6pG1i9x3pZ +5bHuCPFiirGHeO7McvoXkz786GaKVzC9DSpnOxJdc4xm6NSVq7lNEnKdVlnpu9BNYoKX2Iq3wvgh +gGEUM66kK6j4NiyoneuPLSwaCWDxczgaolEWpiMyDVDb7dNuLAbriL8ig8mmeju31oNvQdpnvEPC +1vAXbWacGRVrGt/uXN/gU0CDDwgooKRrHfTBb1/s9lYZ8ZqOBU0yLvpuP6+K9hLFsvIjeNhBi0KL +MlOuWRn3FRwx5oHXjl0YImUx0+gLzjGchrgzca026ETmYJzPD+IpuKzNi8AFn048Thd63OdD86M6 +84zE8yQm0VqXdbbgvub2pKVnS76icBGdeTHHXTKspUmr4NYo/furFLKiMdQzFjHJNcdAnMhltBJK +0/IKX3DVFqvPJ2dLE7bDBkH0l/PJ29074+F0CsGYOxsb7U3myTUncYfXqnLLfa6sJybX4g+hmcjO +kMRBfA1JellfRRKJcyRpxdS4rIl6FdmQCWjo/o9Qz7yKffoP4JHjOvABcRn4CZIT2RH4jnxmfpVG +qgLaAvQBNfuO6X0/Ux02nb4FKx3vgP+XnkX0QW9pLy/NsXgdN24dD3LxO2Nwil7Zlc1dqtP3d7/h +kzp1/+7hGBuY4pk0XD/0Ao/oTe/XGrfyM773aB7iUhgkpy+dwAMalxMP0DrBcsVw/6p25+/hobP9 +GBknrWExDhLJ1bwt1NcCNblaFbMKCyvmX0PeRaQ= +""") + +##file distutils.cfg +DISTUTILS_CFG = convert(""" +eJxNj00KwkAMhfc9xYNuxe4Ft57AjYiUtDO1wXSmNJnK3N5pdSEEAu8nH6lxHVlRhtDHMPATA4uH +xJ4EFmGbvfJiicSHFRzUSISMY6hq3GLCRLnIvSTnEefN0FIjw5tF0Hkk9Q5dRunBsVoyFi24aaLg +9FDOlL0FPGluf4QjcInLlxd6f6rqkgPu/5nHLg0cXCscXoozRrP51DRT3j9QNl99AP53T2Q= +""") + +##file activate_this.py +ACTIVATE_THIS = convert(""" +eJyNU01v2zAMvetXEB4K21jnDOstQA4dMGCHbeihlyEIDMWmE62yJEiKE//7kXKdpEWLzYBt8evx +kRSzLPs6wiEoswM8YdMpjUXcq1Dz6RZa1cSiTkJdr86GsoTRHuCotBayiWqQEYGtMCgfD1KjGYBe +5a3p0cRKiEe2NtLAFikftnDco0ko/SFEVgEZ8aRCZDIPY9xbA8pE9M4jfW/B2CjiHq9zbJVZuOQq +siwTIvpxKYCembPAU4Muwi/Z4zfvrZ/MXipKeB8C+qisSZYiWfjJfs+0/MFMdWn1hJcO5U7G/SLa +xVx8zU6VG/PXLXvfsyyzUqjeWR8hjGE+2iCE1W1tQ82hsCJN9dzKaoexyB/uH79TnjwvxcW0ntSb +yZ8jq1Z5Q1UXsyy3gf9nbjTEj7NzQMfCJa/YSmrQ+2D/BqfiOi6sclrGzvoeVivIj8rcfcmnIQRF +7XCyeZI7DFe5/lhlCs5PRf5QW66VXT/NrlQ46oD/D6InkOmi3IQcbhKxAX2g4a+Xd5s3UtCtG2py +m8eg6WYWqR6SL5OjKMGfSrYt/6kxxQtOpeAgj1LXBNmpE2ElmCSIy5H0zFd8gJ924HWijWhb2hRC +6wNEm1QdDZtuSZcEprIUBo/XRNcbQe1OUbQ/r3hPTaPJJDNtFLu8KHV5XoNr3Eo6h6YtOKw8e8yw +VF5PnJ+ts3a9/Mz38RpG/AUSzYUW +""") + +##file python-config +PYTHON_CONFIG = convert(""" +eJyNVV1P2zAUfc+v8ODBiSABxlulTipbO6p1LWqBgVhlhcZpPYUkctzSivHfd6+dpGloGH2Ja/ue +e+65Hz78xNhtf3x90xmw7vCWsRPGLvpDNuz87MKfdKMWSWxZ4ilNpCLZJiuWc66SVFUOZkkcirll +rfxIBAzOMtImDzSVPBRrekwoX/OZu/0r4lm0DHiG60g86u8sjPw5rCyy86NRkB8QuuBRSqfAKESn +3orLTCQxE3GYkC9tYp8fk89OSwNsmXgizrhUtnumeSgeo5GbLUMk49Rv+2nK48Cm/qMwfp333J2/ +dVcAGE0CIQHBsgIeEr4Wij0LtWDLzJ9ze5YEvH2WI6CHTAVcSu9ZCsXtgxu81CIvp6/k4eXsdfo7 +PvDCRD75yi41QitfzlcPp1OI7i/1/iQitqnr0iMgQ+A6wa+IKwwdxyk9IiXNAzgquTFU8NIxAVjM +osm1Zz526e+shQ4hKRVci69nPC3Kw4NQEmkQ65E7OodxorSvxjvpBjQHDmWFIQ1mlmzlS5vedseT +/mgIEsMJ7Lxz2bLAF9M5xeLEhdbHxpWOw0GdkJApMVBRF1y+a0z3c9WZPAXGFcFrJgCIB+024uad +0CrzmEoRa3Ub4swNIHPGf7QDV+2uj2OiFWsChgCwjKqN6rp5izpbH6Wc1O1TclQTP/XVwi6anTr1 +1sbubjZLI1+VptPSdCfwnFBrB1jvebrTA9uUhU2/9gad7xPqeFkaQcnnLbCViZK8d7R1kxzFrIJV +8EaLYmKYpvGVkig+3C5HCXbM1jGCGekiM2pRCVPyRyXYdPf6kcbWEQ36F5V4Gq9N7icNNw+JHwRE +LTgxRXACpvnQv/PuT0xCCAywY/K4hE6Now2qDwaSE5FB+1agsoUveYDepS83qFcF1NufvULD3fTl +g6Hgf7WBt6lzMeiyyWVn3P1WVbwaczHmTzE9A5SyItTVgFYyvs/L/fXlaNgbw8v3azT+0eikVlWD +/vBHbzQumP23uBCjsYdrL9OWARwxs/nuLOzeXbPJTa/Xv6sUmQir5pC1YRLz3eA+CD8Z0XpcW8v9 +MZWF36ryyXXf3yBIz6nzqz8Muyz0m5Qj7OexfYo/Ph3LqvkHUg7AuA== +""") + +MH_MAGIC = 0xfeedface +MH_CIGAM = 0xcefaedfe +MH_MAGIC_64 = 0xfeedfacf +MH_CIGAM_64 = 0xcffaedfe +FAT_MAGIC = 0xcafebabe +BIG_ENDIAN = '>' +LITTLE_ENDIAN = '<' +LC_LOAD_DYLIB = 0xc +maxint = majver == 3 and getattr(sys, 'maxsize') or getattr(sys, 'maxint') + + +class fileview(object): + """ + A proxy for file-like objects that exposes a given view of a file. + Modified from macholib. + """ + + def __init__(self, fileobj, start=0, size=maxint): + if isinstance(fileobj, fileview): + self._fileobj = fileobj._fileobj + else: + self._fileobj = fileobj + self._start = start + self._end = start + size + self._pos = 0 + + def __repr__(self): + return '' % ( + self._start, self._end, self._fileobj) + + def tell(self): + return self._pos + + def _checkwindow(self, seekto, op): + if not (self._start <= seekto <= self._end): + raise IOError("%s to offset %d is outside window [%d, %d]" % ( + op, seekto, self._start, self._end)) + + def seek(self, offset, whence=0): + seekto = offset + if whence == os.SEEK_SET: + seekto += self._start + elif whence == os.SEEK_CUR: + seekto += self._start + self._pos + elif whence == os.SEEK_END: + seekto += self._end + else: + raise IOError("Invalid whence argument to seek: %r" % (whence,)) + self._checkwindow(seekto, 'seek') + self._fileobj.seek(seekto) + self._pos = seekto - self._start + + def write(self, bytes): + here = self._start + self._pos + self._checkwindow(here, 'write') + self._checkwindow(here + len(bytes), 'write') + self._fileobj.seek(here, os.SEEK_SET) + self._fileobj.write(bytes) + self._pos += len(bytes) + + def read(self, size=maxint): + assert size >= 0 + here = self._start + self._pos + self._checkwindow(here, 'read') + size = min(size, self._end - here) + self._fileobj.seek(here, os.SEEK_SET) + bytes = self._fileobj.read(size) + self._pos += len(bytes) + return bytes + + +def read_data(file, endian, num=1): + """ + Read a given number of 32-bits unsigned integers from the given file + with the given endianness. + """ + res = struct.unpack(endian + 'L' * num, file.read(num * 4)) + if len(res) == 1: + return res[0] + return res + + +def mach_o_change(path, what, value): + """ + Replace a given name (what) in any LC_LOAD_DYLIB command found in + the given binary with a new name (value), provided it's shorter. + """ + + def do_macho(file, bits, endian): + # Read Mach-O header (the magic number is assumed read by the caller) + cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags = read_data(file, endian, 6) + # 64-bits header has one more field. + if bits == 64: + read_data(file, endian) + # The header is followed by ncmds commands + for n in range(ncmds): + where = file.tell() + # Read command header + cmd, cmdsize = read_data(file, endian, 2) + if cmd == LC_LOAD_DYLIB: + # The first data field in LC_LOAD_DYLIB commands is the + # offset of the name, starting from the beginning of the + # command. + name_offset = read_data(file, endian) + file.seek(where + name_offset, os.SEEK_SET) + # Read the NUL terminated string + load = file.read(cmdsize - name_offset).decode() + load = load[:load.index('\0')] + # If the string is what is being replaced, overwrite it. + if load == what: + file.seek(where + name_offset, os.SEEK_SET) + file.write(value.encode() + '\0'.encode()) + # Seek to the next command + file.seek(where + cmdsize, os.SEEK_SET) + + def do_file(file, offset=0, size=maxint): + file = fileview(file, offset, size) + # Read magic number + magic = read_data(file, BIG_ENDIAN) + if magic == FAT_MAGIC: + # Fat binaries contain nfat_arch Mach-O binaries + nfat_arch = read_data(file, BIG_ENDIAN) + for n in range(nfat_arch): + # Read arch header + cputype, cpusubtype, offset, size, align = read_data(file, BIG_ENDIAN, 5) + do_file(file, offset, size) + elif magic == MH_MAGIC: + do_macho(file, 32, BIG_ENDIAN) + elif magic == MH_CIGAM: + do_macho(file, 32, LITTLE_ENDIAN) + elif magic == MH_MAGIC_64: + do_macho(file, 64, BIG_ENDIAN) + elif magic == MH_CIGAM_64: + do_macho(file, 64, LITTLE_ENDIAN) + + assert(len(what) >= len(value)) + + with open(path, 'r+b') as f: + do_file(f) + + +if __name__ == '__main__': + main() + +# TODO: +# Copy python.exe.manifest +# Monkeypatch distutils.sysconfig diff --git a/virtualenv_embedded/activate.bat b/virtualenv_embedded/activate.bat new file mode 100644 index 000000000..ed42021cb --- /dev/null +++ b/virtualenv_embedded/activate.bat @@ -0,0 +1,30 @@ +@echo off +set "VIRTUAL_ENV=__VIRTUAL_ENV__" + +if defined _OLD_VIRTUAL_PROMPT ( + set "PROMPT=%_OLD_VIRTUAL_PROMPT%" +) else ( + if not defined PROMPT ( + set "PROMPT=$P$G" + ) + set "_OLD_VIRTUAL_PROMPT=%PROMPT%" +) +set "PROMPT=__VIRTUAL_WINPROMPT__ %PROMPT%" + +REM Don't use () to avoid problems with them in %PATH% +if defined _OLD_VIRTUAL_PYTHONHOME goto ENDIFVHOME + set "_OLD_VIRTUAL_PYTHONHOME=%PYTHONHOME%" +:ENDIFVHOME + +set PYTHONHOME= + +REM if defined _OLD_VIRTUAL_PATH ( +if not defined _OLD_VIRTUAL_PATH goto ENDIFVPATH1 + set "PATH=%_OLD_VIRTUAL_PATH%" +:ENDIFVPATH1 +REM ) else ( +if defined _OLD_VIRTUAL_PATH goto ENDIFVPATH2 + set "_OLD_VIRTUAL_PATH=%PATH%" +:ENDIFVPATH2 + +set "PATH=%VIRTUAL_ENV%\__BIN_NAME__;%PATH%" diff --git a/virtualenv_embedded/activate.csh b/virtualenv_embedded/activate.csh new file mode 100644 index 000000000..864865b17 --- /dev/null +++ b/virtualenv_embedded/activate.csh @@ -0,0 +1,36 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. +# Created by Davide Di Blasi . + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate && unalias pydoc' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV "__VIRTUAL_ENV__" + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/__BIN_NAME__:$PATH" + + + +if ("__VIRTUAL_PROMPT__" != "") then + set env_name = "__VIRTUAL_PROMPT__" +else + set env_name = `basename "$VIRTUAL_ENV"` +endif + +# Could be in a non-interactive environment, +# in which case, $prompt is undefined and we wouldn't +# care about the prompt anyway. +if ( $?prompt ) then + set _OLD_VIRTUAL_PROMPT="$prompt" + set prompt = "[$env_name] $prompt" +endif + +unset env_name + +alias pydoc python -m pydoc + +rehash + diff --git a/virtualenv_embedded/activate.fish b/virtualenv_embedded/activate.fish new file mode 100644 index 000000000..818739e61 --- /dev/null +++ b/virtualenv_embedded/activate.fish @@ -0,0 +1,76 @@ +# This file must be used using `. bin/activate.fish` *within a running fish ( http://fishshell.com ) session*. +# Do not run it directly. + +function deactivate -d 'Exit virtualenv mode and return to the normal environment.' + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + # Set an empty local `$fish_function_path` to allow the removal of `fish_prompt` using `functions -e`. + set -l fish_function_path + + # Erase virtualenv's `fish_prompt` and restore the original. + functions -e fish_prompt + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + set -e _OLD_FISH_PROMPT_OVERRIDE + end + + set -e VIRTUAL_ENV + + if test "$argv[1]" != 'nondestructive' + # Self-destruct! + functions -e pydoc + functions -e deactivate + end +end + +# Unset irrelevant variables. +deactivate nondestructive + +set -gx VIRTUAL_ENV "__VIRTUAL_ENV__" + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/__BIN_NAME__" $PATH + +# Unset `$PYTHONHOME` if set. +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +function pydoc + python -m pydoc $argv +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # Copy the current `fish_prompt` function as `_old_fish_prompt`. + functions -c fish_prompt _old_fish_prompt + + function fish_prompt + # Save the current $status, for fish_prompts that display it. + set -l old_status $status + + # Prompt override provided? + # If not, just prepend the environment name. + if test -n "__VIRTUAL_PROMPT__" + printf '%s%s' "__VIRTUAL_PROMPT__" (set_color normal) + else + printf '%s(%s) ' (set_color normal) (basename "$VIRTUAL_ENV") + end + + # Restore the original $status + echo "exit $old_status" | source + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" +end diff --git a/virtualenv_embedded/activate.ps1 b/virtualenv_embedded/activate.ps1 new file mode 100644 index 000000000..0f4adf19f --- /dev/null +++ b/virtualenv_embedded/activate.ps1 @@ -0,0 +1,150 @@ +๏ปฟ# This file must be dot sourced from PoSh; you cannot run it +# directly. Do this: . ./activate.ps1 + +# FIXME: clean up unused vars. +$script:THIS_PATH = $myinvocation.mycommand.path +$script:BASE_DIR = split-path (resolve-path "$THIS_PATH/..") -Parent +$script:DIR_NAME = split-path $BASE_DIR -Leaf + +function global:deactivate ( [switch] $NonDestructive ){ + + if ( test-path variable:_OLD_VIRTUAL_PATH ) { + $env:PATH = $variable:_OLD_VIRTUAL_PATH + remove-variable "_OLD_VIRTUAL_PATH" -scope global + } + + if ( test-path function:_old_virtual_prompt ) { + $function:prompt = $function:_old_virtual_prompt + remove-item function:\_old_virtual_prompt + } + + if ($env:VIRTUAL_ENV) { + $old_env = split-path $env:VIRTUAL_ENV -leaf + remove-item env:VIRTUAL_ENV -erroraction silentlycontinue + } + + if ( !$NonDestructive ) { + # Self destruct! + remove-item function:deactivate + } +} + +# unset irrelevant variables +deactivate -nondestructive + +$VIRTUAL_ENV = $BASE_DIR +$env:VIRTUAL_ENV = $VIRTUAL_ENV + +$global:_OLD_VIRTUAL_PATH = $env:PATH +$env:PATH = "$env:VIRTUAL_ENV/Scripts;" + $env:PATH +if (! $env:VIRTUAL_ENV_DISABLE_PROMPT) { + function global:_old_virtual_prompt { "" } + $function:_old_virtual_prompt = $function:prompt + function global:prompt { + # Add a prefix to the current prompt, but don't discard it. + write-host "($(split-path $env:VIRTUAL_ENV -leaf)) " -nonewline + & $function:_old_virtual_prompt + } +} + +# SIG # Begin signature block +# MIISeAYJKoZIhvcNAQcCoIISaTCCEmUCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB +# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR +# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUS5reBwSg3zOUwhXf2jPChZzf +# yPmggg6tMIIGcDCCBFigAwIBAgIBJDANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQG +# EwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERp +# Z2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2Vy +# dGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDcxMDI0MjIwMTQ2WhcNMTcxMDI0MjIw +# MTQ2WjCBjDELMAkGA1UEBhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzAp +# BgNVBAsTIlNlY3VyZSBEaWdpdGFsIENlcnRpZmljYXRlIFNpZ25pbmcxODA2BgNV +# BAMTL1N0YXJ0Q29tIENsYXNzIDIgUHJpbWFyeSBJbnRlcm1lZGlhdGUgT2JqZWN0 +# IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyiOLIjUemqAbPJ1J +# 0D8MlzgWKbr4fYlbRVjvhHDtfhFN6RQxq0PjTQxRgWzwFQNKJCdU5ftKoM5N4YSj +# Id6ZNavcSa6/McVnhDAQm+8H3HWoD030NVOxbjgD/Ih3HaV3/z9159nnvyxQEckR +# ZfpJB2Kfk6aHqW3JnSvRe+XVZSufDVCe/vtxGSEwKCaNrsLc9pboUoYIC3oyzWoU +# TZ65+c0H4paR8c8eK/mC914mBo6N0dQ512/bkSdaeY9YaQpGtW/h/W/FkbQRT3sC +# pttLVlIjnkuY4r9+zvqhToPjxcfDYEf+XD8VGkAqle8Aa8hQ+M1qGdQjAye8OzbV +# uUOw7wIDAQABo4IB6TCCAeUwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +# AQYwHQYDVR0OBBYEFNBOD0CZbLhLGW87KLjg44gHNKq3MB8GA1UdIwQYMBaAFE4L +# 7xqkQFulF2mHMMo0aEPQQa7yMD0GCCsGAQUFBwEBBDEwLzAtBggrBgEFBQcwAoYh +# aHR0cDovL3d3dy5zdGFydHNzbC5jb20vc2ZzY2EuY3J0MFsGA1UdHwRUMFIwJ6Al +# oCOGIWh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3Nmc2NhLmNybDAnoCWgI4YhaHR0 +# cDovL2NybC5zdGFydHNzbC5jb20vc2ZzY2EuY3JsMIGABgNVHSAEeTB3MHUGCysG +# AQQBgbU3AQIBMGYwLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29t +# L3BvbGljeS5wZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cuc3RhcnRzc2wuY29t +# L2ludGVybWVkaWF0ZS5wZGYwEQYJYIZIAYb4QgEBBAQDAgABMFAGCWCGSAGG+EIB +# DQRDFkFTdGFydENvbSBDbGFzcyAyIFByaW1hcnkgSW50ZXJtZWRpYXRlIE9iamVj +# dCBTaWduaW5nIENlcnRpZmljYXRlczANBgkqhkiG9w0BAQUFAAOCAgEAcnMLA3Va +# N4OIE9l4QT5OEtZy5PByBit3oHiqQpgVEQo7DHRsjXD5H/IyTivpMikaaeRxIv95 +# baRd4hoUcMwDj4JIjC3WA9FoNFV31SMljEZa66G8RQECdMSSufgfDYu1XQ+cUKxh +# D3EtLGGcFGjjML7EQv2Iol741rEsycXwIXcryxeiMbU2TPi7X3elbwQMc4JFlJ4B +# y9FhBzuZB1DV2sN2irGVbC3G/1+S2doPDjL1CaElwRa/T0qkq2vvPxUgryAoCppU +# FKViw5yoGYC+z1GaesWWiP1eFKAL0wI7IgSvLzU3y1Vp7vsYaxOVBqZtebFTWRHt +# XjCsFrrQBngt0d33QbQRI5mwgzEp7XJ9xu5d6RVWM4TPRUsd+DDZpBHm9mszvi9g +# VFb2ZG7qRRXCSqys4+u/NLBPbXi/m/lU00cODQTlC/euwjk9HQtRrXQ/zqsBJS6U +# J+eLGw1qOfj+HVBl/ZQpfoLk7IoWlRQvRL1s7oirEaqPZUIWY/grXq9r6jDKAp3L +# ZdKQpPOnnogtqlU4f7/kLjEJhrrc98mrOWmVMK/BuFRAfQ5oDUMnVmCzAzLMjKfG +# cVW/iMew41yfhgKbwpfzm3LBr1Zv+pEBgcgW6onRLSAn3XHM0eNtz+AkxH6rRf6B +# 2mYhLEEGLapH8R1AMAo4BbVFOZR5kXcMCwowggg1MIIHHaADAgECAgIEuDANBgkq +# hkiG9w0BAQUFADCBjDELMAkGA1UEBhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0 +# ZC4xKzApBgNVBAsTIlNlY3VyZSBEaWdpdGFsIENlcnRpZmljYXRlIFNpZ25pbmcx +# ODA2BgNVBAMTL1N0YXJ0Q29tIENsYXNzIDIgUHJpbWFyeSBJbnRlcm1lZGlhdGUg +# T2JqZWN0IENBMB4XDTExMTIwMzE1MzQxOVoXDTEzMTIwMzE0NTgwN1owgYwxIDAe +# BgNVBA0TFzU4MTc5Ni1HaDd4Zkp4a3hRU0lPNEUwMQswCQYDVQQGEwJERTEPMA0G +# A1UECBMGQmVybGluMQ8wDQYDVQQHEwZCZXJsaW4xFjAUBgNVBAMTDUphbm5pcyBM +# ZWlkZWwxITAfBgkqhkiG9w0BCQEWEmphbm5pc0BsZWlkZWwuaW5mbzCCAiIwDQYJ +# KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMcPeABYdN7nPq/AkZ/EkyUBGx/l2Yui +# Lfm8ZdLG0ulMb/kQL3fRY7sUjYPyn9S6PhqqlFnNoGHJvbbReCdUC9SIQYmOEjEA +# raHfb7MZU10NjO4U2DdGucj2zuO5tYxKizizOJF0e4yRQZVxpUGdvkW/+GLjCNK5 +# L7mIv3Z1dagxDKHYZT74HXiS4VFUwHF1k36CwfM2vsetdm46bdgSwV+BCMmZICYT +# IJAS9UQHD7kP4rik3bFWjUx08NtYYFAVOd/HwBnemUmJe4j3IhZHr0k1+eDG8hDH +# KVvPgLJIoEjC4iMFk5GWsg5z2ngk0LLu3JZMtckHsnnmBPHQK8a3opUNd8hdMNJx +# gOwKjQt2JZSGUdIEFCKVDqj0FmdnDMPfwy+FNRtpBMl1sz78dUFhSrnM0D8NXrqa +# 4rG+2FoOXlmm1rb6AFtpjAKksHRpYcPk2DPGWp/1sWB+dUQkS3gOmwFzyqeTuXpT +# 0juqd3iAxOGx1VRFQ1VHLLf3AzV4wljBau26I+tu7iXxesVucSdsdQu293jwc2kN +# xK2JyHCoZH+RyytrwS0qw8t7rMOukU9gwP8mn3X6mgWlVUODMcHTULjSiCEtvyZ/ +# aafcwjUbt4ReEcnmuZtWIha86MTCX7U7e+cnpWG4sIHPnvVTaz9rm8RyBkIxtFCB +# nQ3FnoQgyxeJAgMBAAGjggOdMIIDmTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIH +# gDAuBgNVHSUBAf8EJDAiBggrBgEFBQcDAwYKKwYBBAGCNwIBFQYKKwYBBAGCNwoD +# DTAdBgNVHQ4EFgQUWyCgrIWo8Ifvvm1/YTQIeMU9nc8wHwYDVR0jBBgwFoAU0E4P +# QJlsuEsZbzsouODjiAc0qrcwggIhBgNVHSAEggIYMIICFDCCAhAGCysGAQQBgbU3 +# AQICMIIB/zAuBggrBgEFBQcCARYiaHR0cDovL3d3dy5zdGFydHNzbC5jb20vcG9s +# aWN5LnBkZjA0BggrBgEFBQcCARYoaHR0cDovL3d3dy5zdGFydHNzbC5jb20vaW50 +# ZXJtZWRpYXRlLnBkZjCB9wYIKwYBBQUHAgIwgeowJxYgU3RhcnRDb20gQ2VydGlm +# aWNhdGlvbiBBdXRob3JpdHkwAwIBARqBvlRoaXMgY2VydGlmaWNhdGUgd2FzIGlz +# c3VlZCBhY2NvcmRpbmcgdG8gdGhlIENsYXNzIDIgVmFsaWRhdGlvbiByZXF1aXJl +# bWVudHMgb2YgdGhlIFN0YXJ0Q29tIENBIHBvbGljeSwgcmVsaWFuY2Ugb25seSBm +# b3IgdGhlIGludGVuZGVkIHB1cnBvc2UgaW4gY29tcGxpYW5jZSBvZiB0aGUgcmVs +# eWluZyBwYXJ0eSBvYmxpZ2F0aW9ucy4wgZwGCCsGAQUFBwICMIGPMCcWIFN0YXJ0 +# Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MAMCAQIaZExpYWJpbGl0eSBhbmQg +# d2FycmFudGllcyBhcmUgbGltaXRlZCEgU2VlIHNlY3Rpb24gIkxlZ2FsIGFuZCBM +# aW1pdGF0aW9ucyIgb2YgdGhlIFN0YXJ0Q29tIENBIHBvbGljeS4wNgYDVR0fBC8w +# LTAroCmgJ4YlaHR0cDovL2NybC5zdGFydHNzbC5jb20vY3J0YzItY3JsLmNybDCB +# iQYIKwYBBQUHAQEEfTB7MDcGCCsGAQUFBzABhitodHRwOi8vb2NzcC5zdGFydHNz +# bC5jb20vc3ViL2NsYXNzMi9jb2RlL2NhMEAGCCsGAQUFBzAChjRodHRwOi8vYWlh +# LnN0YXJ0c3NsLmNvbS9jZXJ0cy9zdWIuY2xhc3MyLmNvZGUuY2EuY3J0MCMGA1Ud +# EgQcMBqGGGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tLzANBgkqhkiG9w0BAQUFAAOC +# AQEAhrzEV6zwoEtKjnFRhCsjwiPykVpo5Eiye77Ve801rQDiRKgSCCiW6g3HqedL +# OtaSs65Sj2pm3Viea4KR0TECLcbCTgsdaHqw2x1yXwWBQWZEaV6EB05lIwfr94P1 +# SFpV43zkuc+bbmA3+CRK45LOcCNH5Tqq7VGTCAK5iM7tvHwFlbQRl+I6VEL2mjpF +# NsuRjDOVrv/9qw/a22YJ9R7Y1D0vUSs3IqZx2KMUaYDP7H2mSRxJO2nADQZBtriF +# gTyfD3lYV12MlIi5CQwe3QC6DrrfSMP33i5Wa/OFJiQ27WPxmScYVhiqozpImFT4 +# PU9goiBv9RKXdgTmZE1PN0NQ5jGCAzUwggMxAgEBMIGTMIGMMQswCQYDVQQGEwJJ +# TDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0 +# YWwgQ2VydGlmaWNhdGUgU2lnbmluZzE4MDYGA1UEAxMvU3RhcnRDb20gQ2xhc3Mg +# MiBQcmltYXJ5IEludGVybWVkaWF0ZSBPYmplY3QgQ0ECAgS4MAkGBSsOAwIaBQCg +# eDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE +# AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMCMGCSqGSIb3DQEJ +# BDEWBBRVGw0FDSiaIi38dWteRUAg/9Pr6DANBgkqhkiG9w0BAQEFAASCAgCInvOZ +# FdaNFzbf6trmFDZKMojyx3UjKMCqNjHVBbuKY0qXwFC/ElYDV1ShJ2CBZbdurydO +# OQ6cIQ0KREOCwmX/xB49IlLHHUxNhEkVv7HGU3EKAFf9IBt9Yr7jikiR9cjIsfHK +# 4cjkoKJL7g28yEpLLkHt1eo37f1Ga9lDWEa5Zq3U5yX+IwXhrUBm1h8Xr033FhTR +# VEpuSz6LHtbrL/zgJnCzJ2ahjtJoYevdcWiNXffosJHFaSfYDDbiNsPRDH/1avmb +# 5j/7BhP8BcBaR6Fp8tFbNGIcWHHGcjqLMnTc4w13b7b4pDhypqElBa4+lCmwdvv9 +# GydYtRgPz8GHeoBoKj30YBlMzRIfFYaIFGIC4Ai3UEXkuH9TxYohVbGm/W0Kl4Lb +# RJ1FwiVcLcTOJdgNId2vQvKc+jtNrjcg5SP9h2v/C4aTx8tyc6tE3TOPh2f9b8DL +# S+SbVArJpuJqrPTxDDoO1QNjTgLcdVYeZDE+r/NjaGZ6cMSd8db3EaG3ijD/0bud +# SItbm/OlNVbQOFRR76D+ZNgPcU5iNZ3bmvQQIg6aSB9MHUpIE/SeCkNl9YeVk1/1 +# GFULgNMRmIYP4KLvu9ylh5Gu3hvD5VNhH6+FlXANwFy07uXks5uF8mfZVxVCnodG +# xkNCx+6PsrA5Z7WP4pXcmYnMn97npP/Q9EHJWw== +# SIG # End signature block diff --git a/virtualenv_embedded/activate.sh b/virtualenv_embedded/activate.sh new file mode 100644 index 000000000..477b7eca2 --- /dev/null +++ b/virtualenv_embedded/activate.sh @@ -0,0 +1,78 @@ +# This file must be used with "source bin/activate" *from bash* +# you cannot run it directly + +deactivate () { + unset -f pydoc >/dev/null 2>&1 + + # reset old environment variables + # ! [ -z ${VAR+_} ] returns true if VAR is declared at all + if ! [ -z "${_OLD_VIRTUAL_PATH+_}" ] ; then + PATH="$_OLD_VIRTUAL_PATH" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if ! [ -z "${_OLD_VIRTUAL_PYTHONHOME+_}" ] ; then + PYTHONHOME="$_OLD_VIRTUAL_PYTHONHOME" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # This should detect bash and zsh, which have a hash command that must + # be called to get it to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ] ; then + hash -r 2>/dev/null + fi + + if ! [ -z "${_OLD_VIRTUAL_PS1+_}" ] ; then + PS1="$_OLD_VIRTUAL_PS1" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + if [ ! "${1-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +VIRTUAL_ENV="__VIRTUAL_ENV__" +export VIRTUAL_ENV + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/__BIN_NAME__:$PATH" +export PATH + +# unset PYTHONHOME if set +if ! [ -z "${PYTHONHOME+_}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="$PYTHONHOME" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then + _OLD_VIRTUAL_PS1="$PS1" + if [ "x__VIRTUAL_PROMPT__" != x ] ; then + PS1="__VIRTUAL_PROMPT__$PS1" + else + PS1="(`basename \"$VIRTUAL_ENV\"`) $PS1" + fi + export PS1 +fi + +# Make sure to unalias pydoc if it's already there +alias pydoc 2>/dev/null >/dev/null && unalias pydoc + +pydoc () { + python -m pydoc "$@" +} + +# This should detect bash and zsh, which have a hash command that must +# be called to get it to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ] ; then + hash -r 2>/dev/null +fi diff --git a/virtualenv_embedded/activate_this.py b/virtualenv_embedded/activate_this.py new file mode 100644 index 000000000..f18193bf8 --- /dev/null +++ b/virtualenv_embedded/activate_this.py @@ -0,0 +1,34 @@ +"""By using execfile(this_file, dict(__file__=this_file)) you will +activate this virtualenv environment. + +This can be used when you must use an existing Python interpreter, not +the virtualenv bin/python +""" + +try: + __file__ +except NameError: + raise AssertionError( + "You must run this like execfile('path/to/activate_this.py', dict(__file__='path/to/activate_this.py'))") +import sys +import os + +old_os_path = os.environ.get('PATH', '') +os.environ['PATH'] = os.path.dirname(os.path.abspath(__file__)) + os.pathsep + old_os_path +base = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if sys.platform == 'win32': + site_packages = os.path.join(base, 'Lib', 'site-packages') +else: + site_packages = os.path.join(base, 'lib', 'python%s' % sys.version[:3], 'site-packages') +prev_sys_path = list(sys.path) +import site +site.addsitedir(site_packages) +sys.real_prefix = sys.prefix +sys.prefix = base +# Move the added items to the front of the path: +new_sys_path = [] +for item in list(sys.path): + if item not in prev_sys_path: + new_sys_path.append(item) + sys.path.remove(item) +sys.path[:0] = new_sys_path diff --git a/virtualenv_embedded/deactivate.bat b/virtualenv_embedded/deactivate.bat new file mode 100644 index 000000000..74d367168 --- /dev/null +++ b/virtualenv_embedded/deactivate.bat @@ -0,0 +1,19 @@ +@echo off + +set VIRTUAL_ENV= + +REM Don't use () to avoid problems with them in %PATH% +if not defined _OLD_VIRTUAL_PROMPT goto ENDIFVPROMPT + set "PROMPT=%_OLD_VIRTUAL_PROMPT%" + set _OLD_VIRTUAL_PROMPT= +:ENDIFVPROMPT + +if not defined _OLD_VIRTUAL_PYTHONHOME goto ENDIFVHOME + set "PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%" + set _OLD_VIRTUAL_PYTHONHOME= +:ENDIFVHOME + +if not defined _OLD_VIRTUAL_PATH goto ENDIFVPATH + set "PATH=%_OLD_VIRTUAL_PATH%" + set _OLD_VIRTUAL_PATH= +:ENDIFVPATH \ No newline at end of file diff --git a/virtualenv_embedded/distutils-init.py b/virtualenv_embedded/distutils-init.py new file mode 100644 index 000000000..29fc1da45 --- /dev/null +++ b/virtualenv_embedded/distutils-init.py @@ -0,0 +1,101 @@ +import os +import sys +import warnings +import imp +import opcode # opcode is not a virtualenv module, so we can use it to find the stdlib + # Important! To work on pypy, this must be a module that resides in the + # lib-python/modified-x.y.z directory + +dirname = os.path.dirname + +distutils_path = os.path.join(os.path.dirname(opcode.__file__), 'distutils') +if os.path.normpath(distutils_path) == os.path.dirname(os.path.normpath(__file__)): + warnings.warn( + "The virtualenv distutils package at %s appears to be in the same location as the system distutils?") +else: + __path__.insert(0, distutils_path) + real_distutils = imp.load_module("_virtualenv_distutils", None, distutils_path, ('', '', imp.PKG_DIRECTORY)) + # Copy the relevant attributes + try: + __revision__ = real_distutils.__revision__ + except AttributeError: + pass + __version__ = real_distutils.__version__ + +from distutils import dist, sysconfig + +try: + basestring +except NameError: + basestring = str + +## patch build_ext (distutils doesn't know how to get the libs directory +## path on windows - it hardcodes the paths around the patched sys.prefix) + +if sys.platform == 'win32': + from distutils.command.build_ext import build_ext as old_build_ext + class build_ext(old_build_ext): + def finalize_options (self): + if self.library_dirs is None: + self.library_dirs = [] + elif isinstance(self.library_dirs, basestring): + self.library_dirs = self.library_dirs.split(os.pathsep) + + self.library_dirs.insert(0, os.path.join(sys.real_prefix, "Libs")) + old_build_ext.finalize_options(self) + + from distutils.command import build_ext as build_ext_module + build_ext_module.build_ext = build_ext + +## distutils.dist patches: + +old_find_config_files = dist.Distribution.find_config_files +def find_config_files(self): + found = old_find_config_files(self) + system_distutils = os.path.join(distutils_path, 'distutils.cfg') + #if os.path.exists(system_distutils): + # found.insert(0, system_distutils) + # What to call the per-user config file + if os.name == 'posix': + user_filename = ".pydistutils.cfg" + else: + user_filename = "pydistutils.cfg" + user_filename = os.path.join(sys.prefix, user_filename) + if os.path.isfile(user_filename): + for item in list(found): + if item.endswith('pydistutils.cfg'): + found.remove(item) + found.append(user_filename) + return found +dist.Distribution.find_config_files = find_config_files + +## distutils.sysconfig patches: + +old_get_python_inc = sysconfig.get_python_inc +def sysconfig_get_python_inc(plat_specific=0, prefix=None): + if prefix is None: + prefix = sys.real_prefix + return old_get_python_inc(plat_specific, prefix) +sysconfig_get_python_inc.__doc__ = old_get_python_inc.__doc__ +sysconfig.get_python_inc = sysconfig_get_python_inc + +old_get_python_lib = sysconfig.get_python_lib +def sysconfig_get_python_lib(plat_specific=0, standard_lib=0, prefix=None): + if standard_lib and prefix is None: + prefix = sys.real_prefix + return old_get_python_lib(plat_specific, standard_lib, prefix) +sysconfig_get_python_lib.__doc__ = old_get_python_lib.__doc__ +sysconfig.get_python_lib = sysconfig_get_python_lib + +old_get_config_vars = sysconfig.get_config_vars +def sysconfig_get_config_vars(*args): + real_vars = old_get_config_vars(*args) + if sys.platform == 'win32': + lib_dir = os.path.join(sys.real_prefix, "libs") + if isinstance(real_vars, dict) and 'LIBDIR' not in real_vars: + real_vars['LIBDIR'] = lib_dir # asked for all + elif isinstance(real_vars, list) and 'LIBDIR' in args: + real_vars = real_vars + [lib_dir] # asked for list + return real_vars +sysconfig_get_config_vars.__doc__ = old_get_config_vars.__doc__ +sysconfig.get_config_vars = sysconfig_get_config_vars diff --git a/virtualenv_embedded/distutils.cfg b/virtualenv_embedded/distutils.cfg new file mode 100644 index 000000000..1af230ec9 --- /dev/null +++ b/virtualenv_embedded/distutils.cfg @@ -0,0 +1,6 @@ +# This is a config file local to this virtualenv installation +# You may include options that will be used by all distutils commands, +# and by easy_install. For instance: +# +# [easy_install] +# find_links = http://mylocalsite diff --git a/virtualenv_embedded/python-config b/virtualenv_embedded/python-config new file mode 100644 index 000000000..5e7a7c901 --- /dev/null +++ b/virtualenv_embedded/python-config @@ -0,0 +1,78 @@ +#!__VIRTUAL_ENV__/__BIN_NAME__/python + +import sys +import getopt +import sysconfig + +valid_opts = ['prefix', 'exec-prefix', 'includes', 'libs', 'cflags', + 'ldflags', 'help'] + +if sys.version_info >= (3, 2): + valid_opts.insert(-1, 'extension-suffix') + valid_opts.append('abiflags') +if sys.version_info >= (3, 3): + valid_opts.append('configdir') + + +def exit_with_usage(code=1): + sys.stderr.write("Usage: {0} [{1}]\n".format( + sys.argv[0], '|'.join('--'+opt for opt in valid_opts))) + sys.exit(code) + +try: + opts, args = getopt.getopt(sys.argv[1:], '', valid_opts) +except getopt.error: + exit_with_usage() + +if not opts: + exit_with_usage() + +pyver = sysconfig.get_config_var('VERSION') +getvar = sysconfig.get_config_var + +opt_flags = [flag for (flag, val) in opts] + +if '--help' in opt_flags: + exit_with_usage(code=0) + +for opt in opt_flags: + if opt == '--prefix': + print(sysconfig.get_config_var('prefix')) + + elif opt == '--exec-prefix': + print(sysconfig.get_config_var('exec_prefix')) + + elif opt in ('--includes', '--cflags'): + flags = ['-I' + sysconfig.get_path('include'), + '-I' + sysconfig.get_path('platinclude')] + if opt == '--cflags': + flags.extend(getvar('CFLAGS').split()) + print(' '.join(flags)) + + elif opt in ('--libs', '--ldflags'): + abiflags = getattr(sys, 'abiflags', '') + libs = ['-lpython' + pyver + abiflags] + libs += getvar('LIBS').split() + libs += getvar('SYSLIBS').split() + # add the prefix/lib/pythonX.Y/config dir, but only if there is no + # shared library in prefix/lib/. + if opt == '--ldflags': + if not getvar('Py_ENABLE_SHARED'): + libs.insert(0, '-L' + getvar('LIBPL')) + if not getvar('PYTHONFRAMEWORK'): + libs.extend(getvar('LINKFORSHARED').split()) + print(' '.join(libs)) + + elif opt == '--extension-suffix': + ext_suffix = sysconfig.get_config_var('EXT_SUFFIX') + if ext_suffix is None: + ext_suffix = sysconfig.get_config_var('SO') + print(ext_suffix) + + elif opt == '--abiflags': + if not getattr(sys, 'abiflags', None): + exit_with_usage() + print(sys.abiflags) + + elif opt == '--configdir': + print(sysconfig.get_config_var('LIBPL')) diff --git a/virtualenv_embedded/site.py b/virtualenv_embedded/site.py new file mode 100644 index 000000000..7969769c3 --- /dev/null +++ b/virtualenv_embedded/site.py @@ -0,0 +1,758 @@ +"""Append module search paths for third-party packages to sys.path. + +**************************************************************** +* This module is automatically imported during initialization. * +**************************************************************** + +In earlier versions of Python (up to 1.5a3), scripts or modules that +needed to use site-specific modules would place ``import site'' +somewhere near the top of their code. Because of the automatic +import, this is no longer necessary (but code that does it still +works). + +This will append site-specific paths to the module search path. On +Unix, it starts with sys.prefix and sys.exec_prefix (if different) and +appends lib/python/site-packages as well as lib/site-python. +It also supports the Debian convention of +lib/python/dist-packages. On other platforms (mainly Mac and +Windows), it uses just sys.prefix (and sys.exec_prefix, if different, +but this is unlikely). The resulting directories, if they exist, are +appended to sys.path, and also inspected for path configuration files. + +FOR DEBIAN, this sys.path is augmented with directories in /usr/local. +Local addons go into /usr/local/lib/python/site-packages +(resp. /usr/local/lib/site-python), Debian addons install into +/usr/{lib,share}/python/dist-packages. + +A path configuration file is a file whose name has the form +.pth; its contents are additional directories (one per line) +to be added to sys.path. Non-existing directories (or +non-directories) are never added to sys.path; no directory is added to +sys.path more than once. Blank lines and lines beginning with +'#' are skipped. Lines starting with 'import' are executed. + +For example, suppose sys.prefix and sys.exec_prefix are set to +/usr/local and there is a directory /usr/local/lib/python2.X/site-packages +with three subdirectories, foo, bar and spam, and two path +configuration files, foo.pth and bar.pth. Assume foo.pth contains the +following: + + # foo package configuration + foo + bar + bletch + +and bar.pth contains: + + # bar package configuration + bar + +Then the following directories are added to sys.path, in this order: + + /usr/local/lib/python2.X/site-packages/bar + /usr/local/lib/python2.X/site-packages/foo + +Note that bletch is omitted because it doesn't exist; bar precedes foo +because bar.pth comes alphabetically before foo.pth; and spam is +omitted because it is not mentioned in either path configuration file. + +After these path manipulations, an attempt is made to import a module +named sitecustomize, which can perform arbitrary additional +site-specific customizations. If this import fails with an +ImportError exception, it is silently ignored. + +""" + +import sys +import os +try: + import __builtin__ as builtins +except ImportError: + import builtins +try: + set +except NameError: + from sets import Set as set + +# Prefixes for site-packages; add additional prefixes like /usr/local here +PREFIXES = [sys.prefix, sys.exec_prefix] +# Enable per user site-packages directory +# set it to False to disable the feature or True to force the feature +ENABLE_USER_SITE = None +# for distutils.commands.install +USER_SITE = None +USER_BASE = None + +_is_64bit = (getattr(sys, 'maxsize', None) or getattr(sys, 'maxint')) > 2**32 +_is_pypy = hasattr(sys, 'pypy_version_info') +_is_jython = sys.platform[:4] == 'java' +if _is_jython: + ModuleType = type(os) + +def makepath(*paths): + dir = os.path.join(*paths) + if _is_jython and (dir == '__classpath__' or + dir.startswith('__pyclasspath__')): + return dir, dir + dir = os.path.abspath(dir) + return dir, os.path.normcase(dir) + +def abs__file__(): + """Set all module' __file__ attribute to an absolute path""" + for m in sys.modules.values(): + if ((_is_jython and not isinstance(m, ModuleType)) or + hasattr(m, '__loader__')): + # only modules need the abspath in Jython. and don't mess + # with a PEP 302-supplied __file__ + continue + f = getattr(m, '__file__', None) + if f is None: + continue + m.__file__ = os.path.abspath(f) + +def removeduppaths(): + """ Remove duplicate entries from sys.path along with making them + absolute""" + # This ensures that the initial path provided by the interpreter contains + # only absolute pathnames, even if we're running from the build directory. + L = [] + known_paths = set() + for dir in sys.path: + # Filter out duplicate paths (on case-insensitive file systems also + # if they only differ in case); turn relative paths into absolute + # paths. + dir, dircase = makepath(dir) + if not dircase in known_paths: + L.append(dir) + known_paths.add(dircase) + sys.path[:] = L + return known_paths + +# XXX This should not be part of site.py, since it is needed even when +# using the -S option for Python. See http://www.python.org/sf/586680 +def addbuilddir(): + """Append ./build/lib. in case we're running in the build dir + (especially for Guido :-)""" + from distutils.util import get_platform + s = "build/lib.%s-%.3s" % (get_platform(), sys.version) + if hasattr(sys, 'gettotalrefcount'): + s += '-pydebug' + s = os.path.join(os.path.dirname(sys.path[-1]), s) + sys.path.append(s) + +def _init_pathinfo(): + """Return a set containing all existing directory entries from sys.path""" + d = set() + for dir in sys.path: + try: + if os.path.isdir(dir): + dir, dircase = makepath(dir) + d.add(dircase) + except TypeError: + continue + return d + +def addpackage(sitedir, name, known_paths): + """Add a new path to known_paths by combining sitedir and 'name' or execute + sitedir if it starts with 'import'""" + if known_paths is None: + _init_pathinfo() + reset = 1 + else: + reset = 0 + fullname = os.path.join(sitedir, name) + try: + f = open(fullname, "rU") + except IOError: + return + try: + for line in f: + if line.startswith("#"): + continue + if line.startswith("import"): + exec(line) + continue + line = line.rstrip() + dir, dircase = makepath(sitedir, line) + if not dircase in known_paths and os.path.exists(dir): + sys.path.append(dir) + known_paths.add(dircase) + finally: + f.close() + if reset: + known_paths = None + return known_paths + +def addsitedir(sitedir, known_paths=None): + """Add 'sitedir' argument to sys.path if missing and handle .pth files in + 'sitedir'""" + if known_paths is None: + known_paths = _init_pathinfo() + reset = 1 + else: + reset = 0 + sitedir, sitedircase = makepath(sitedir) + if not sitedircase in known_paths: + sys.path.append(sitedir) # Add path component + try: + names = os.listdir(sitedir) + except os.error: + return + names.sort() + for name in names: + if name.endswith(os.extsep + "pth"): + addpackage(sitedir, name, known_paths) + if reset: + known_paths = None + return known_paths + +def addsitepackages(known_paths, sys_prefix=sys.prefix, exec_prefix=sys.exec_prefix): + """Add site-packages (and possibly site-python) to sys.path""" + prefixes = [os.path.join(sys_prefix, "local"), sys_prefix] + if exec_prefix != sys_prefix: + prefixes.append(os.path.join(exec_prefix, "local")) + + for prefix in prefixes: + if prefix: + if sys.platform in ('os2emx', 'riscos') or _is_jython: + sitedirs = [os.path.join(prefix, "Lib", "site-packages")] + elif _is_pypy: + sitedirs = [os.path.join(prefix, 'site-packages')] + elif sys.platform == 'darwin' and prefix == sys_prefix: + + if prefix.startswith("/System/Library/Frameworks/"): # Apple's Python + + sitedirs = [os.path.join("/Library/Python", sys.version[:3], "site-packages"), + os.path.join(prefix, "Extras", "lib", "python")] + + else: # any other Python distros on OSX work this way + sitedirs = [os.path.join(prefix, "lib", + "python" + sys.version[:3], "site-packages")] + + elif os.sep == '/': + sitedirs = [os.path.join(prefix, + "lib", + "python" + sys.version[:3], + "site-packages"), + os.path.join(prefix, "lib", "site-python"), + os.path.join(prefix, "python" + sys.version[:3], "lib-dynload")] + lib64_dir = os.path.join(prefix, "lib64", "python" + sys.version[:3], "site-packages") + if (os.path.exists(lib64_dir) and + os.path.realpath(lib64_dir) not in [os.path.realpath(p) for p in sitedirs]): + if _is_64bit: + sitedirs.insert(0, lib64_dir) + else: + sitedirs.append(lib64_dir) + try: + # sys.getobjects only available in --with-pydebug build + sys.getobjects + sitedirs.insert(0, os.path.join(sitedirs[0], 'debug')) + except AttributeError: + pass + # Debian-specific dist-packages directories: + sitedirs.append(os.path.join(prefix, "local/lib", + "python" + sys.version[:3], + "dist-packages")) + if sys.version[0] == '2': + sitedirs.append(os.path.join(prefix, "lib", + "python" + sys.version[:3], + "dist-packages")) + else: + sitedirs.append(os.path.join(prefix, "lib", + "python" + sys.version[0], + "dist-packages")) + sitedirs.append(os.path.join(prefix, "lib", "dist-python")) + else: + sitedirs = [prefix, os.path.join(prefix, "lib", "site-packages")] + if sys.platform == 'darwin': + # for framework builds *only* we add the standard Apple + # locations. Currently only per-user, but /Library and + # /Network/Library could be added too + if 'Python.framework' in prefix: + home = os.environ.get('HOME') + if home: + sitedirs.append( + os.path.join(home, + 'Library', + 'Python', + sys.version[:3], + 'site-packages')) + for sitedir in sitedirs: + if os.path.isdir(sitedir): + addsitedir(sitedir, known_paths) + return None + +def check_enableusersite(): + """Check if user site directory is safe for inclusion + + The function tests for the command line flag (including environment var), + process uid/gid equal to effective uid/gid. + + None: Disabled for security reasons + False: Disabled by user (command line option) + True: Safe and enabled + """ + if hasattr(sys, 'flags') and getattr(sys.flags, 'no_user_site', False): + return False + + if hasattr(os, "getuid") and hasattr(os, "geteuid"): + # check process uid == effective uid + if os.geteuid() != os.getuid(): + return None + if hasattr(os, "getgid") and hasattr(os, "getegid"): + # check process gid == effective gid + if os.getegid() != os.getgid(): + return None + + return True + +def addusersitepackages(known_paths): + """Add a per user site-package to sys.path + + Each user has its own python directory with site-packages in the + home directory. + + USER_BASE is the root directory for all Python versions + + USER_SITE is the user specific site-packages directory + + USER_SITE/.. can be used for data. + """ + global USER_BASE, USER_SITE, ENABLE_USER_SITE + env_base = os.environ.get("PYTHONUSERBASE", None) + + def joinuser(*args): + return os.path.expanduser(os.path.join(*args)) + + #if sys.platform in ('os2emx', 'riscos'): + # # Don't know what to put here + # USER_BASE = '' + # USER_SITE = '' + if os.name == "nt": + base = os.environ.get("APPDATA") or "~" + if env_base: + USER_BASE = env_base + else: + USER_BASE = joinuser(base, "Python") + USER_SITE = os.path.join(USER_BASE, + "Python" + sys.version[0] + sys.version[2], + "site-packages") + else: + if env_base: + USER_BASE = env_base + else: + USER_BASE = joinuser("~", ".local") + USER_SITE = os.path.join(USER_BASE, "lib", + "python" + sys.version[:3], + "site-packages") + + if ENABLE_USER_SITE and os.path.isdir(USER_SITE): + addsitedir(USER_SITE, known_paths) + if ENABLE_USER_SITE: + for dist_libdir in ("lib", "local/lib"): + user_site = os.path.join(USER_BASE, dist_libdir, + "python" + sys.version[:3], + "dist-packages") + if os.path.isdir(user_site): + addsitedir(user_site, known_paths) + return known_paths + + + +def setBEGINLIBPATH(): + """The OS/2 EMX port has optional extension modules that do double duty + as DLLs (and must use the .DLL file extension) for other extensions. + The library search path needs to be amended so these will be found + during module import. Use BEGINLIBPATH so that these are at the start + of the library search path. + + """ + dllpath = os.path.join(sys.prefix, "Lib", "lib-dynload") + libpath = os.environ['BEGINLIBPATH'].split(';') + if libpath[-1]: + libpath.append(dllpath) + else: + libpath[-1] = dllpath + os.environ['BEGINLIBPATH'] = ';'.join(libpath) + + +def setquit(): + """Define new built-ins 'quit' and 'exit'. + These are simply strings that display a hint on how to exit. + + """ + if os.sep == ':': + eof = 'Cmd-Q' + elif os.sep == '\\': + eof = 'Ctrl-Z plus Return' + else: + eof = 'Ctrl-D (i.e. EOF)' + + class Quitter(object): + def __init__(self, name): + self.name = name + def __repr__(self): + return 'Use %s() or %s to exit' % (self.name, eof) + def __call__(self, code=None): + # Shells like IDLE catch the SystemExit, but listen when their + # stdin wrapper is closed. + try: + sys.stdin.close() + except: + pass + raise SystemExit(code) + builtins.quit = Quitter('quit') + builtins.exit = Quitter('exit') + + +class _Printer(object): + """interactive prompt objects for printing the license text, a list of + contributors and the copyright notice.""" + + MAXLINES = 23 + + def __init__(self, name, data, files=(), dirs=()): + self.__name = name + self.__data = data + self.__files = files + self.__dirs = dirs + self.__lines = None + + def __setup(self): + if self.__lines: + return + data = None + for dir in self.__dirs: + for filename in self.__files: + filename = os.path.join(dir, filename) + try: + fp = open(filename, "rU") + data = fp.read() + fp.close() + break + except IOError: + pass + if data: + break + if not data: + data = self.__data + self.__lines = data.split('\n') + self.__linecnt = len(self.__lines) + + def __repr__(self): + self.__setup() + if len(self.__lines) <= self.MAXLINES: + return "\n".join(self.__lines) + else: + return "Type %s() to see the full %s text" % ((self.__name,)*2) + + def __call__(self): + self.__setup() + prompt = 'Hit Return for more, or q (and Return) to quit: ' + lineno = 0 + while 1: + try: + for i in range(lineno, lineno + self.MAXLINES): + print(self.__lines[i]) + except IndexError: + break + else: + lineno += self.MAXLINES + key = None + while key is None: + try: + key = raw_input(prompt) + except NameError: + key = input(prompt) + if key not in ('', 'q'): + key = None + if key == 'q': + break + +def setcopyright(): + """Set 'copyright' and 'credits' in __builtin__""" + builtins.copyright = _Printer("copyright", sys.copyright) + if _is_jython: + builtins.credits = _Printer( + "credits", + "Jython is maintained by the Jython developers (www.jython.org).") + elif _is_pypy: + builtins.credits = _Printer( + "credits", + "PyPy is maintained by the PyPy developers: http://pypy.org/") + else: + builtins.credits = _Printer("credits", """\ + Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands + for supporting Python development. See www.python.org for more information.""") + here = os.path.dirname(os.__file__) + builtins.license = _Printer( + "license", "See http://www.python.org/%.3s/license.html" % sys.version, + ["LICENSE.txt", "LICENSE"], + [os.path.join(here, os.pardir), here, os.curdir]) + + +class _Helper(object): + """Define the built-in 'help'. + This is a wrapper around pydoc.help (with a twist). + + """ + + def __repr__(self): + return "Type help() for interactive help, " \ + "or help(object) for help about object." + def __call__(self, *args, **kwds): + import pydoc + return pydoc.help(*args, **kwds) + +def sethelper(): + builtins.help = _Helper() + +def aliasmbcs(): + """On Windows, some default encodings are not provided by Python, + while they are always available as "mbcs" in each locale. Make + them usable by aliasing to "mbcs" in such a case.""" + if sys.platform == 'win32': + import locale, codecs + enc = locale.getdefaultlocale()[1] + if enc.startswith('cp'): # "cp***" ? + try: + codecs.lookup(enc) + except LookupError: + import encodings + encodings._cache[enc] = encodings._unknown + encodings.aliases.aliases[enc] = 'mbcs' + +def setencoding(): + """Set the string encoding used by the Unicode implementation. The + default is 'ascii', but if you're willing to experiment, you can + change this.""" + encoding = "ascii" # Default value set by _PyUnicode_Init() + if 0: + # Enable to support locale aware default string encodings. + import locale + loc = locale.getdefaultlocale() + if loc[1]: + encoding = loc[1] + if 0: + # Enable to switch off string to Unicode coercion and implicit + # Unicode to string conversion. + encoding = "undefined" + if encoding != "ascii": + # On Non-Unicode builds this will raise an AttributeError... + sys.setdefaultencoding(encoding) # Needs Python Unicode build ! + + +def execsitecustomize(): + """Run custom site specific code, if available.""" + try: + import sitecustomize + except ImportError: + pass + +def virtual_install_main_packages(): + f = open(os.path.join(os.path.dirname(__file__), 'orig-prefix.txt')) + sys.real_prefix = f.read().strip() + f.close() + pos = 2 + hardcoded_relative_dirs = [] + if sys.path[0] == '': + pos += 1 + if _is_jython: + paths = [os.path.join(sys.real_prefix, 'Lib')] + elif _is_pypy: + if sys.version_info > (3, 2): + cpyver = '%d' % sys.version_info[0] + elif sys.pypy_version_info >= (1, 5): + cpyver = '%d.%d' % sys.version_info[:2] + else: + cpyver = '%d.%d.%d' % sys.version_info[:3] + paths = [os.path.join(sys.real_prefix, 'lib_pypy'), + os.path.join(sys.real_prefix, 'lib-python', cpyver)] + if sys.pypy_version_info < (1, 9): + paths.insert(1, os.path.join(sys.real_prefix, + 'lib-python', 'modified-%s' % cpyver)) + hardcoded_relative_dirs = paths[:] # for the special 'darwin' case below + # + # This is hardcoded in the Python executable, but relative to sys.prefix: + for path in paths[:]: + plat_path = os.path.join(path, 'plat-%s' % sys.platform) + if os.path.exists(plat_path): + paths.append(plat_path) + elif sys.platform == 'win32': + paths = [os.path.join(sys.real_prefix, 'Lib'), os.path.join(sys.real_prefix, 'DLLs')] + else: + paths = [os.path.join(sys.real_prefix, 'lib', 'python'+sys.version[:3])] + hardcoded_relative_dirs = paths[:] # for the special 'darwin' case below + lib64_path = os.path.join(sys.real_prefix, 'lib64', 'python'+sys.version[:3]) + if os.path.exists(lib64_path): + if _is_64bit: + paths.insert(0, lib64_path) + else: + paths.append(lib64_path) + # This is hardcoded in the Python executable, but relative to + # sys.prefix. Debian change: we need to add the multiarch triplet + # here, which is where the real stuff lives. As per PEP 421, in + # Python 3.3+, this lives in sys.implementation, while in Python 2.7 + # it lives in sys. + try: + arch = getattr(sys, 'implementation', sys)._multiarch + except AttributeError: + # This is a non-multiarch aware Python. Fallback to the old way. + arch = sys.platform + plat_path = os.path.join(sys.real_prefix, 'lib', + 'python'+sys.version[:3], + 'plat-%s' % arch) + if os.path.exists(plat_path): + paths.append(plat_path) + # This is hardcoded in the Python executable, but + # relative to sys.prefix, so we have to fix up: + for path in list(paths): + tk_dir = os.path.join(path, 'lib-tk') + if os.path.exists(tk_dir): + paths.append(tk_dir) + + # These are hardcoded in the Apple's Python executable, + # but relative to sys.prefix, so we have to fix them up: + if sys.platform == 'darwin': + hardcoded_paths = [os.path.join(relative_dir, module) + for relative_dir in hardcoded_relative_dirs + for module in ('plat-darwin', 'plat-mac', 'plat-mac/lib-scriptpackages')] + + for path in hardcoded_paths: + if os.path.exists(path): + paths.append(path) + + sys.path.extend(paths) + +def force_global_eggs_after_local_site_packages(): + """ + Force easy_installed eggs in the global environment to get placed + in sys.path after all packages inside the virtualenv. This + maintains the "least surprise" result that packages in the + virtualenv always mask global packages, never the other way + around. + + """ + egginsert = getattr(sys, '__egginsert', 0) + for i, path in enumerate(sys.path): + if i > egginsert and path.startswith(sys.prefix): + egginsert = i + sys.__egginsert = egginsert + 1 + +def virtual_addsitepackages(known_paths): + force_global_eggs_after_local_site_packages() + return addsitepackages(known_paths, sys_prefix=sys.real_prefix) + +def fixclasspath(): + """Adjust the special classpath sys.path entries for Jython. These + entries should follow the base virtualenv lib directories. + """ + paths = [] + classpaths = [] + for path in sys.path: + if path == '__classpath__' or path.startswith('__pyclasspath__'): + classpaths.append(path) + else: + paths.append(path) + sys.path = paths + sys.path.extend(classpaths) + +def execusercustomize(): + """Run custom user specific code, if available.""" + try: + import usercustomize + except ImportError: + pass + + +def main(): + global ENABLE_USER_SITE + virtual_install_main_packages() + abs__file__() + paths_in_sys = removeduppaths() + if (os.name == "posix" and sys.path and + os.path.basename(sys.path[-1]) == "Modules"): + addbuilddir() + if _is_jython: + fixclasspath() + GLOBAL_SITE_PACKAGES = not os.path.exists(os.path.join(os.path.dirname(__file__), 'no-global-site-packages.txt')) + if not GLOBAL_SITE_PACKAGES: + ENABLE_USER_SITE = False + if ENABLE_USER_SITE is None: + ENABLE_USER_SITE = check_enableusersite() + paths_in_sys = addsitepackages(paths_in_sys) + paths_in_sys = addusersitepackages(paths_in_sys) + if GLOBAL_SITE_PACKAGES: + paths_in_sys = virtual_addsitepackages(paths_in_sys) + if sys.platform == 'os2emx': + setBEGINLIBPATH() + setquit() + setcopyright() + sethelper() + aliasmbcs() + setencoding() + execsitecustomize() + if ENABLE_USER_SITE: + execusercustomize() + # Remove sys.setdefaultencoding() so that users cannot change the + # encoding after initialization. The test for presence is needed when + # this module is run as a script, because this code is executed twice. + if hasattr(sys, "setdefaultencoding"): + del sys.setdefaultencoding + +main() + +def _script(): + help = """\ + %s [--user-base] [--user-site] + + Without arguments print some useful information + With arguments print the value of USER_BASE and/or USER_SITE separated + by '%s'. + + Exit codes with --user-base or --user-site: + 0 - user site directory is enabled + 1 - user site directory is disabled by user + 2 - uses site directory is disabled by super user + or for security reasons + >2 - unknown error + """ + args = sys.argv[1:] + if not args: + print("sys.path = [") + for dir in sys.path: + print(" %r," % (dir,)) + print("]") + def exists(path): + if os.path.isdir(path): + return "exists" + else: + return "doesn't exist" + print("USER_BASE: %r (%s)" % (USER_BASE, exists(USER_BASE))) + print("USER_SITE: %r (%s)" % (USER_SITE, exists(USER_BASE))) + print("ENABLE_USER_SITE: %r" % ENABLE_USER_SITE) + sys.exit(0) + + buffer = [] + if '--user-base' in args: + buffer.append(USER_BASE) + if '--user-site' in args: + buffer.append(USER_SITE) + + if buffer: + print(os.pathsep.join(buffer)) + if ENABLE_USER_SITE: + sys.exit(0) + elif ENABLE_USER_SITE is False: + sys.exit(1) + elif ENABLE_USER_SITE is None: + sys.exit(2) + else: + sys.exit(3) + else: + import textwrap + print(textwrap.dedent(help % (sys.argv[0], os.pathsep))) + sys.exit(10) + +if __name__ == '__main__': + _script() diff --git a/src/virtualenv/config/cli/__init__.py b/virtualenv_support/__init__.py similarity index 100% rename from src/virtualenv/config/cli/__init__.py rename to virtualenv_support/__init__.py diff --git a/virtualenv_support/argparse-1.4.0-py2.py3-none-any.whl b/virtualenv_support/argparse-1.4.0-py2.py3-none-any.whl new file mode 100644 index 000000000..dfef51d44 Binary files /dev/null and b/virtualenv_support/argparse-1.4.0-py2.py3-none-any.whl differ diff --git a/virtualenv_support/pip-9.0.1-py2.py3-none-any.whl b/virtualenv_support/pip-9.0.1-py2.py3-none-any.whl new file mode 100644 index 000000000..4b8ecc69d Binary files /dev/null and b/virtualenv_support/pip-9.0.1-py2.py3-none-any.whl differ diff --git a/virtualenv_support/setuptools-28.8.0-py2.py3-none-any.whl b/virtualenv_support/setuptools-28.8.0-py2.py3-none-any.whl new file mode 100644 index 000000000..502e3cb41 Binary files /dev/null and b/virtualenv_support/setuptools-28.8.0-py2.py3-none-any.whl differ diff --git a/virtualenv_support/wheel-0.29.0-py2.py3-none-any.whl b/virtualenv_support/wheel-0.29.0-py2.py3-none-any.whl new file mode 100644 index 000000000..506d5e520 Binary files /dev/null and b/virtualenv_support/wheel-0.29.0-py2.py3-none-any.whl differ