[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: \"Bug report\"\ndescription: Create a report to help us improve\nlabels: ['🐛 bug']\nbody:\n  - type: markdown\n    attributes:\n      value: \"Thank you for taking the time to report a bug. Please provide as much information as possible to help us understand and resolve the issue.\"\n  - type: textarea\n    id: describe-bug\n    attributes:\n      label: Describe the bug\n      description: \"A clear and concise description of what the bug is.\"\n      placeholder: \"Describe the bug...\"\n    validations:\n      required: true\n  - type: textarea\n    id: reproduce-bug\n    attributes:\n      label: To reproduce\n      description: \"Steps to reproduce the behavior.\"\n      placeholder: \"Steps to reproduce the behavior...\"\n    validations:\n      required: true\n  - type: textarea\n    id: expected-behavior\n    attributes:\n      label: Expected Behavior\n      description: \"A clear and concise description of what you expected to happen.\"\n      placeholder: \"Explain what you expected to happen...\"\n    validations:\n      required: true\n  - type: textarea\n    id: \"environment-info\"\n    attributes:\n      label: Environment Information\n      description: \"Paste the output of `pdm info && pdm info --env`\"\n      placeholder: \"Paste the output of `pdm info && pdm info --env`\"\n    validations:\n      required: true\n  - type: textarea\n    id: \"pdm-debug-output\"\n    attributes:\n      label: \"Verbose Command Output\"\n      description: \"Please provide the command output with `-v`.\"\n      placeholder: \"Add the command output with `-v`...\"\n    validations:\n      required: false\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: Additional Context\n      description: \"Add any other context about the problem here.\"\n      placeholder: \"Additional details...\"\n    validations:\n      required: false\n  - type: checkboxes\n    id: willing-to-submit-pr\n    attributes:\n      label: \"Are you willing to submit a PR to fix this bug?\"\n      description: \"Let us know if you are willing to contribute a fix by submitting a Pull Request.\"\n      options:\n        - label: \"Yes, I would like to submit a PR.\"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: \"Feature / Enhancement Proposal\"\ndescription: Suggest an idea for this project\nlabels: ['⭐ enhancement']\nbody:\n  - type: markdown\n    attributes:\n      value: \"Thank you for suggesting a new feature. Please fill out the details below to help us understand your idea better.\"\n\n  - type: textarea\n    id: feature-description\n    attributes:\n      label: Feature Description\n      description: \"A detailed description of the feature you would like to see.\"\n      placeholder: \"Describe the feature you'd like...\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: problem-solution\n    attributes:\n      label: Problem and Solution\n      description: \"Describe the problem that this feature would solve. Explain how you envision it working.\"\n      placeholder: \"What problem does this feature solve? How do you envision it working?\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: Additional Context\n      description: \"Add any other context or screenshots about the feature request here.\"\n      placeholder: \"Add any other context or screenshots about the feature request here.\"\n    validations:\n      required: false\n\n  - type: checkboxes\n    id: willing-to-contribute\n    attributes:\n      label: \"Are you willing to contribute to the development of this feature?\"\n      description: \"Let us know if you are willing to help by contributing code or other resources.\"\n      options:\n        - label: \"Yes, I am willing to contribute to the development of this feature.\"\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "## Pull Request Checklist\n\n- [ ] A news fragment is added in `news/` describing what is new.\n- [ ] Test cases added for changed code.\n\n## Describe what you have changed in this PR.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\r\nupdates:\r\n  # Maintain dependencies for workflow actions\r\n  - package-ecosystem: \"github-actions\"\r\n    directory: \"/\"\r\n    schedule:\r\n      interval: \"monthly\"\r\n    labels:\r\n      - \"github_actions\"\r\n    groups:\r\n      actions:\r\n        patterns:\r\n          - \"*\"\r\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: Tests\n\non:\n  pull_request:\n    branches:\n      - main\n      - dev\n      - \"maintain/*\"\n    paths-ignore:\n      - \"docs/**\"\n      - \"news/**\"\n      - \"*.md\"\n  push:\n    branches:\n      - main\n      - dev\n      - \"maintain/*\"\n    paths-ignore:\n      - \"docs/**\"\n      - \"news/**\"\n      - \"*.md\"\n\nconcurrency:\n  group: ${{ github.event.number || github.run_id }}\n  cancel-in-progress: true\n\njobs:\n  Testing:\n    env:\n      PYTHONDEVMODE: 1\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [3.9, \"3.10\", 3.11, 3.12, 3.13, 3.14]\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        install-via: [pip]\n        include:\n          - python-version: 3.12\n            os: ubuntu-latest\n            install-via: script\n          - python-version: pypy-3.11\n            os: ubuntu-latest\n            install-via: pip\n    steps:\n      - uses: actions/checkout@v6.0.2\n\n      - name: Setup Python Versions\n        uses: actions/setup-python@v6\n        with:\n          python-version: |\n            3.9\n            3.10\n            3.11\n            3.12\n            3.13\n            3.14\n          allow-prereleases: true\n        if: matrix.os != 'macos-latest'\n      - name: Setup Python Versions\n        uses: actions/setup-python@v6\n        with:\n          python-version: |\n            3.10\n            3.11\n            3.12\n            3.13\n            3.14\n          allow-prereleases: true\n        if: matrix.os == 'macos-latest'\n      - name: Setup Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v6\n        with:\n          python-version: ${{ matrix.python-version }}\n          cache: pip\n          allow-prereleases: true\n\n      - name: Cache venv\n        uses: actions/cache@v5.0.3\n        with:\n          path: .venv\n          key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('pdm.lock') }}\n      - name: Install uv\n        uses: astral-sh/setup-uv@v7.3.1\n        with:\n          version: \"latest\"\n      - name: Install current PDM via pip\n        if: matrix.install-via == 'pip'\n        run: python -m pip install -U .\n      - name: Install current PDM via script\n        if: matrix.install-via == 'script'\n        run: |\n          shasum -a256 --check install-pdm.py.sha256\n          python install-pdm.py --version head\n          echo \"$HOME/.local/bin\" >> $GITHUB_PATH\n      - name: Install Dev Dependencies\n        run: |\n          pdm install -v -Gtest\n          pdm run pip install -U setuptools\n          pdm info\n      # - name: Setup tmate session\n      #   uses: mxschmitt/action-tmate@v3.22\n      - name: Run Tests\n        run: pdm run pytest -n auto --cov=pdm --cov-config=pyproject.toml --cov-report=xml tests\n\n      - name: Upload coverage to Codecov\n        uses: codecov/codecov-action@v5.5.2\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          file: ./coverage.xml\n          flags: unittests\n\n  Pack:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6.0.2\n        with:\n          fetch-depth: 0\n      - uses: actions/setup-python@v6\n        with:\n          python-version: 3.x\n      - name: Install PDM\n        run: |\n          python -m pip install .\n          pdm self add pdm-packer\n      - name: Pack pdm\n        run: pdm pack\n\n      - name: Test zipapp\n        run: python pdm.pyz --version\n"
  },
  {
    "path": ".github/workflows/claude.yml",
    "content": "name: Claude Code\n\non:\n  issue_comment:\n    types: [created]\n  pull_request_review_comment:\n    types: [created]\n  issues:\n    types: [opened, assigned]\n  pull_request_review:\n    types: [submitted]\n\njobs:\n  claude:\n    if: |\n      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||\n      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||\n      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||\n      (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: read\n      issues: read\n      id-token: write\n      actions: read # Required for Claude to read CI results on PRs\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6.0.2\n        with:\n          fetch-depth: 1\n\n      - name: Run Claude Code\n        id: claude\n        uses: anthropics/claude-code-action@v1.0.64\n        with:\n          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}\n\n          # This is an optional setting that allows Claude to read CI results on PRs\n          additional_permissions: |\n            actions: read\n          \n          # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)\n          # model: \"claude-opus-4-20250514\"\n          \n          # Optional: Customize the trigger phrase (default: @claude)\n          # trigger_phrase: \"/claude\"\n          \n          # Optional: Trigger when specific user is assigned to an issue\n          # assignee_trigger: \"claude-bot\"\n          \n          # Optional: Allow Claude to run specific commands\n          # allowed_tools: \"Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)\"\n          \n          # Optional: Add custom instructions for Claude to customize its behavior for your project\n          # custom_instructions: |\n          #   Follow our coding standards\n          #   Ensure all new code has tests\n          #   Use TypeScript for new files\n          \n          # Optional: Custom environment variables for Claude\n          # claude_env: |\n          #   NODE_ENV: test\n\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - \"*\"\n\ndefaults:\n  run:\n    # make sure to work on Windows\n    shell: bash\n\njobs:\n  release-pypi:\n    name: release-pypi\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write\n      contents: write\n\n    steps:\n      - uses: actions/checkout@v6.0.2\n\n      - uses: actions/setup-python@v6\n        with:\n          python-version: \"3.11\"\n          cache: pip\n\n      - name: Check prerelease\n        id: check_version\n        run: |\n          if [[ \"${{ github.ref }}\" =~ ^refs/tags/[0-9.]+$ ]]; then\n            echo \"PRERELEASE=false\" >> $GITHUB_OUTPUT\n          else\n            echo \"PRERELEASE=true\" >> $GITHUB_OUTPUT\n          fi\n\n      - name: Build artifacts\n        run: |\n          pipx run build\n\n      - name: Upload artifacts\n        uses: actions/upload-artifact@v7.0.0\n        with:\n          name: pdm-wheel\n          path: dist/*.whl\n          if-no-files-found: error\n          retention-days: 15\n\n      - name: Test Build\n        run: |\n          python -m pip install \"pdm[locked] @ file://$(ls ${GITHUB_WORKSPACE}/dist/*.whl)\"\n          pdm --help\n\n      - name: Publish package distributions to PyPI\n        run: pdm publish --no-build\n\n      - name: Get Changelog\n        id: get-changelog\n        run: |\n          awk '/## Release/{if (flag==1)exit;else;flag=1;next} flag' CHANGELOG.md > .changelog.md\n\n      - name: Create Release\n        uses: actions/create-release@main\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: ${{ github.ref }}\n          release_name: v${{ github.ref }}\n          body_path: .changelog.md\n          draft: false\n          prerelease: ${{ steps.check_version.outputs.PRERELEASE }}\n\n      - name: Trigger Bucket Update\n        uses: benc-uk/workflow-dispatch@v1.3.1\n        with:\n          workflow: Excavator\n          repo: frostming/scoop-frostming\n          token: ${{ secrets.G_T }}\n          ref: master\n\n  binary:\n    needs: release-pypi\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        os:\n          [\n            \"ubuntu-24.04\",\n            \"ubuntu-24.04-arm\",\n            \"windows-2025\",\n            \"macos-15-intel\",\n            \"macos-15\",\n          ]\n\n    env:\n      PYAPP_REPO: pyapp\n      PYAPP_VERSION: \"0.27.0\"\n      PYAPP_PROJECT_NAME: pdm\n      PYAPP_PROJECT_VERSION: ${{ github.ref_name }}\n      PYAPP_SELF_COMMAND: app # since `self` has been taken in `pdm`\n      PYAPP_DISTRIBUTION_EMBED: true\n      PYAPP_PROJECT_FEATURES: locked\n      SOURCE_FILE: ${{ matrix.os != 'windows-2025' && 'pyapp' || 'pyapp.exe' }}\n      TARGET_FILE: ${{ matrix.os != 'windows-2025' && 'pdm' || 'pdm.exe' }}\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6.0.2\n\n      - name: Fetch PyApp\n        run: >-\n          mkdir $PYAPP_REPO && curl -L\n          https://github.com/ofek/pyapp/releases/download/v$PYAPP_VERSION/source.tar.gz\n          |\n          tar --strip-components=1 -xzf - -C $PYAPP_REPO\n\n      - name: Setup Rust\n        uses: dtolnay/rust-toolchain@stable\n\n      - name: Run sccache-cache\n        uses: mozilla-actions/sccache-action@v0.0.9\n        if: matrix.os != 'macos-15-intel'\n\n      - name: Set sccache env\n        if: matrix.os != 'macos-15-intel'\n        run: |\n          echo \"SCCACHE_GHA_ENABLED=true\" >> $GITHUB_ENV\n          echo \"RUSTC_WRAPPER=sccache\" >> $GITHUB_ENV\n\n      - name: Download artifacts\n        uses: actions/download-artifact@v8.0.0\n        with:\n          name: pdm-wheel\n          path: dist\n\n      - name: Configure embedded wheel\n        run: |\n          cd dist\n          wheel=\"$(echo *.whl)\"\n          mv $wheel ../$PYAPP_REPO\n          echo \"PYAPP_PROJECT_PATH=$wheel\" >> $GITHUB_ENV\n          echo \"TARGET_TRIPLE=$(rustc --version --verbose | grep \"host\" | awk '{print $2}')\" >> $GITHUB_ENV\n\n      - name: Build\n        run: |\n          cd $PYAPP_REPO\n          cargo build --release\n          mv target/release/$SOURCE_FILE ../$TARGET_FILE\n\n      - name: Upload Assets\n        uses: actions/upload-artifact@v7.0.0\n        with:\n          name: pdm-${{ github.ref_name }}-${{ env.TARGET_TRIPLE }}\n          path: ${{ env.TARGET_FILE }}\n          if-no-files-found: error\n          retention-days: 15\n\n      - name: Create and Upload Archive with Checksum\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          UPLOAD_FILE: pdm-${{ github.ref_name }}-${{ env.TARGET_TRIPLE }}.tar.gz\n          CHECKSUM_FILE: pdm-${{ github.ref_name }}-${{ env.TARGET_TRIPLE }}.tar.gz.sha256\n        run: |\n          tar -czf $UPLOAD_FILE $TARGET_FILE\n          if command -v sha256sum &> /dev/null; then\n            sha256sum $UPLOAD_FILE > $CHECKSUM_FILE\n          else\n            shasum -a 256 $UPLOAD_FILE > $CHECKSUM_FILE\n          fi\n          gh release upload ${{ github.ref_name }} $UPLOAD_FILE $CHECKSUM_FILE --clobber\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\ndocs/site\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\n/venv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n.vscode/\ncaches/\n.idea/\n__pypackages__\n.pdm.toml\n.pdm-python\ntemp.py\n\n# Pyannotate generated stubs\ntype_info.json\n.pdm-build/\nsrc/pdm/VERSION\n.zed/\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "ci:\n  autoupdate_schedule: monthly\nrepos:\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: 'v0.15.4'\n    hooks:\n      - id: ruff\n        args: [--fix, --exit-non-zero-on-fix, --show-fixes]\n      - id: ruff-format\n\n  - repo: https://github.com/codespell-project/codespell\n    rev: v2.4.1\n    hooks:\n      - id: codespell  # See pyproject.toml for args\n        additional_dependencies:\n          - tomli\n\n  - repo: https://github.com/pre-commit/mirrors-mypy\n    rev: v1.19.1\n    hooks:\n      - id: mypy\n        args: [src]\n        pass_filenames: false\n        additional_dependencies:\n          - types-requests\n          - types-certifi\n          - pytest\n"
  },
  {
    "path": ".pre-commit-hooks.yaml",
    "content": "- id: pdm-lock-check\n  name: pdm-lock-check\n  description: run pdm lock --check to validate config\n  entry: pdm lock --check\n  language: python\n  language_version: python3\n  pass_filenames: false\n  files: ^pyproject.toml$\n- id: pdm-export\n  name: pdm-export-lock\n  description: export locked packages to requirements.txt or setup.py\n  entry: pdm export\n  language: python\n  language_version: python3\n  pass_filenames: false\n  files: ^pdm.lock$\n- id: pdm-sync\n  name: pdm-sync\n  description: sync current working set with pdm.lock\n  entry: pdm sync\n  language: python\n  language_version: python3\n  pass_filenames: false\n  stages:\n    - post-checkout\n    - post-merge\n    - post-rewrite\n  always_run: true\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "# Read the Docs configuration file for MkDocs projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the version of Python and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.12\"\n  jobs:\n    post_create_environment:\n      - python install-pdm.py --path ~/.local/pdm\n      - ~/.local/pdm/bin/pdm --version\n    post_install:\n      - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH ~/.local/pdm/bin/pdm install -dG doc\n\nmkdocs:\n  configuration: mkdocs.yml\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# AGENTS.md\n\nThis file provides guidance to AI coding assistants when working with code in this repository.\n\n## Project Overview\n\nPDM (Python Dependency Manager) is a modern Python package and dependency manager that supports the latest PEP standards (PEP 517, PEP 621). It provides fast dependency resolution, flexible plugin system, and centralized cache management similar to pnpm.\n\n## Core Architecture\n\n### Key Components\n\n1. **Project Management** (`src/pdm/project/`): Handles pyproject.toml parsing, project configuration, and metadata management\n2. **Dependency Resolution** (`src/pdm/resolver/`): Fast dependency resolver using resolvelib with custom optimizations for binary distributions\n3. **Environment Management** (`src/pdm/environments/`): Manages Python environments (virtualenv, PEP 582, system)\n4. **Installer System** (`src/pdm/installers/`): Installs and uninstalls packages into the site-packages directory with centralized cache support\n5. **CLI System** (`src/pdm/cli/commands/`): Command-line interface using argparse with plugin support\n6. **Repository Models** (`src/pdm/models/repositories/`): PyPI repository interaction and package finder\n7. **Build System** (`src/pdm/builders/`): PEP 517 build front-end for creating wheels and sdists\n\n### Command Entry Points\n\nAll CLI commands are in `src/pdm/cli/commands/` with command registration in `src/pdm/core.py`. Commands inherit from `BaseCommand` and use decorator patterns for common options.\n\n## Development Commands\n\n### Setup Development Environment\n```bash\n# Install development dependencies\npdm install\n```\n\n### Run Tests\n```bash\n# Run all tests\npdm run test\n\n# Run tests in parallel\npdm run test -n auto\n```\n\nMost of the time, you can exclude tests with \"integration\" mark to save runtime:\n\n```bash\npdm run test -n auto -m \"not integration\"\n```\n\n### Code Quality\n```bash\n# Run linting (ruff-format + codespell + mypy)\npdm run lint\n```\n\n### Documentation\n```bash\n# Serve documentation locally\npdm run doc\n```\n\n### Contribution Guidelines\n\nRefer to [CONTRIBUTING.md](CONTRIBUTING.md)\n\n## Important Files\n\n- `pyproject.toml`: Project configuration and dependencies\n- `src/pdm/core.py`: Main application entry point and command registration\n- `src/pdm/project/__init__.py`: Project class managing project state\n- `src/pdm/cli/commands/base.py`: Base command class for all CLI commands\n- `.pre-commit-config.yaml`: Code quality hooks (ruff, mypy, codespell)\n\n## Common Development Tasks\n\n### Adding a New Command\n1. Create new file in `src/pdm/cli/commands/`\n2. Inherit from `BaseCommand`\n3. Register in `src/pdm/core.py`\n\n### Debugging Resolution Issues\n- Set `PDM_DEBUG=1` environment variable for verbose output\n- Check `pdm.lock` for resolved versions\n- Use `pdm lock --check` to verify lock file\n\n### Working with Lock Files\n\nPDM uses its own lock file format (`pdm.lock`) that includes:\n- Exact versions with hashes\n- Environment markers\n- Cross-platform support\n- Group dependencies\n\n### Update dependencies\n\n```bash\n# Add a new dependency to default group\npdm add <package_name>\n\n# Update all dependencies\npdm update\n\n# Remove a dependency\npdm remove <package_name>\n\n# Add a new dependency to given group\npdm add <package_name> --group <group_name>\n```\n\n## Architecture Patterns\n\n- **Dependency Injection**: Core class passed to commands\n- **Signal System**: Event-driven architecture for plugins\n- **Repository Pattern**: Abstract repository interface for package sources\n- **Strategy Pattern**: Different environment backends (venv, conda, etc.)\n- **Chain of Responsibility**: Middleware system for HTTP client\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## Release v2.26.6 (2026-01-22)\n\n### Bug Fixes\n\n- Support `packaging==26.0` changes for version comparison ([#3729](https://github.com/pdm-project/pdm/issues/3729))\n\n\n## Release v2.26.5 (2026-01-21)\n\n### Bug Fixes\n\n- Respect the project path when using cookiecutter template in `pdm init` command. ([#3721](https://github.com/pdm-project/pdm/issues/3721))\n- Fix a bug that `resolution.excludes` is not applied when evaluating candidates from the lock file. ([#3726](https://github.com/pdm-project/pdm/issues/3726))\n\n### Documentation\n\n- Remove chatbot from the docs page footer. ([#3722](https://github.com/pdm-project/pdm/issues/3722))\n- Generate llms.txt for docs powered by `mkdocs-llmstxt`. ([#3723](https://github.com/pdm-project/pdm/issues/3723))\n\n\n## Release v2.26.4 (2026-01-09)\n\n### Bug Fixes\n\n- Make sure cursor closing for fixing PyPy different gc mode also add PyPy in CI. ([#3708](https://github.com/pdm-project/pdm/issues/3708))\n- Fix a bug that old HTTP cache directories cause PDM to crash when trying to clear them. ([#3715](https://github.com/pdm-project/pdm/issues/3715))\n\n\n## Release v2.26.3 (2025-12-24)\n\n### Features & Improvements\n\n- Port to `hishel` 1.0.0. ([#3700](https://github.com/pdm-project/pdm/issues/3700))\n\n### Bug Fixes\n\n- Update `.gitignore` file in the default template. ([#3686](https://github.com/pdm-project/pdm/issues/3686))\n- Correct the sysconfig variables for Python standalone build installations. ([#3693](https://github.com/pdm-project/pdm/issues/3693))\n- Ignore `packages.vcs.requested-revision` if it's None when formatting pylock.toml. ([#3694](https://github.com/pdm-project/pdm/issues/3694))\n- Fix test failures with uv test cases using non-venv Python interpreters. ([#3698](https://github.com/pdm-project/pdm/issues/3698))\n\n\n## Release v2.26.2 (2025-11-24)\n\n### Features & Improvements\n\n- Only parse TOML document with `tomlkit` when writing is required. ([#3672](https://github.com/pdm-project/pdm/issues/3672))\n- Add SHA256 checksums for binary releases during the release workflow and create an installer script that downloads binaries from GitHub releases with automatic platform detection and checksum verification. ([#3679](https://github.com/pdm-project/pdm/issues/3679))\n\n### Bug Fixes\n\n- Fix test_use_python_write_file_multiple_versions to match PDM's actual behavior. ([#3660](https://github.com/pdm-project/pdm/issues/3660))\n- Correctly calculate the venv path for `UV_PROJECT_ENVIRONMENT` env var when using uv mode. ([#3675](https://github.com/pdm-project/pdm/issues/3675))\n- Ensure `implementation.gil_disabled` is a boolean in `get_current_env_spec`. This fix an issue that free-threaded wheels get rejected incorrectly. ([#3677](https://github.com/pdm-project/pdm/issues/3677))\n- Fix CLI help formatting on Python 3.14+. ([#3683](https://github.com/pdm-project/pdm/issues/3683))\n- Make `PdmBasicAuth` a `cached_property` to accelerate execution. ([#3684](https://github.com/pdm-project/pdm/issues/3684))\n\n### Removals and Deprecations\n\n- Add deprecation warning for `pdm search` command as PyPI no longer supports search API. ([#3674](https://github.com/pdm-project/pdm/issues/3674))\n\n### Miscellany\n\n- Add tests to utils.fs_supports_link_method and utils.convert_to_datetime. ([#3541](https://github.com/pdm-project/pdm/issues/3541))\n\n\n## Release v2.26.1 (2025-10-29)\n\n### Bug Fixes\n\n- Substitute missing env vars with empty string in `expand_env_vars`. ([#3653](https://github.com/pdm-project/pdm/issues/3653))\n- Constrained hishel to be less than 1.0.0 due to its refactor ([#3657](https://github.com/pdm-project/pdm/issues/3657))\n\n\n## Release v2.26.0 (2025-10-11)\n\n### Features & Improvements\n\n- Limit the log file size to 100MB and truncate the log output if exceeded. ([#3633](https://github.com/pdm-project/pdm/issues/3633))\n- Speed up dependency resolution in the bad path by skipping candidates of the same version when resolving. ([#3647](https://github.com/pdm-project/pdm/issues/3647))\n\n### Bug Fixes\n\n- Reload project files after running hook scripts. ([#3615](https://github.com/pdm-project/pdm/issues/3615))\n- Fix a bug when using UV as the resolver does not respect the venv.location configuration. ([#3616](https://github.com/pdm-project/pdm/issues/3616))\n- Fix `publish --skip-existing` for Nexus Repository OSS >= 3.70 ([#3617](https://github.com/pdm-project/pdm/issues/3617))\n- Fix a resolution failure when both prerelease and non-prerelease requirements exist. ([#3634](https://github.com/pdm-project/pdm/issues/3634))\n- Ignore invalid `python` requirement during locking. ([#3635](https://github.com/pdm-project/pdm/issues/3635))\n- Isolate PDM loggers with the root logger to avoid log leakage. ([#3637](https://github.com/pdm-project/pdm/issues/3637))\n- Fix a crash when resolving URL dependencies under `use_uv=true`. ([#3640](https://github.com/pdm-project/pdm/issues/3640))\n\n\n## Release v2.25.9 (2025-08-22)\n\nNo significant changes.\n\n\n## Release v2.25.8 (2025-08-22)\n\n### Bug Fixes\n\n- Fix a careless error by fast apply in AI coding. ([#3612](https://github.com/pdm-project/pdm/issues/3612))\n\n\n## Release v2.25.7 (2025-08-22)\n\n### Features & Improvements\n\n- Show the path to site-packages in the output of `pdm info`. ([#3600](https://github.com/pdm-project/pdm/issues/3600))\n\n### Bug Fixes\n\n- Fix `uv python dir` path resolution on Windows ([#3603](https://github.com/pdm-project/pdm/issues/3603))\n- Strip local version in version specifiers when writing package locks. ([#3605](https://github.com/pdm-project/pdm/issues/3605))\n- Show an error message when 'default' is used in optional dependencies or dependency groups. ([#3609](https://github.com/pdm-project/pdm/issues/3609))\n- Prevent hash clearing when appending to lockfile with env_spec. ([#3610](https://github.com/pdm-project/pdm/issues/3610))\n\n\n## Release v2.25.6 (2025-08-14)\n\n### Features & Improvements\n\n- The `pdm python install -v` command now shows the download URL for the Python interpreter. ([#3552](https://github.com/pdm-project/pdm/issues/3552))\n\n### Bug Fixes\n\n- Ensure `make_array` always returns a tomlkit array type. ([#3586](https://github.com/pdm-project/pdm/issues/3586))\n- Preserve multi-line help text in the CLI help output. ([#3587](https://github.com/pdm-project/pdm/issues/3587))\n- Re-caculate artifact files and hashes when the lock target changes. ([#3595](https://github.com/pdm-project/pdm/issues/3595))\n\n### Dependencies\n\n- Require packaging>22.0 and remove conditional PACKAGING_22 version checks. ([#3601](https://github.com/pdm-project/pdm/issues/3601))\n- Bump truststore to version 0.10.4. ([#3602](https://github.com/pdm-project/pdm/issues/3602))\n\n\n## Release v2.25.5 (2025-07-30)\n\n### Features & Improvements\n\n- Tell the difference between free-threaded Python and normal ones. Users need to request for free-threaded versions explicitly by adding `t` to the version string, otherwise the normal build will be preferred. ([#3562](https://github.com/pdm-project/pdm/issues/3562))\n\n### Bug Fixes\n\n- Fix a bug that editable local package URLs are empty when using `pylock.toml`. ([#3565](https://github.com/pdm-project/pdm/issues/3565))\n- Fix a bug where `pdm export` with `--lockfile pylock.toml` produced empty requirements.txt files due to missing group information extraction from pylock format markers. ([#3573](https://github.com/pdm-project/pdm/issues/3573))\n- Read metadata from installed distribution when using reuse-installed strategy. ([#3579](https://github.com/pdm-project/pdm/issues/3579))\n- Fix a lockfile writing error when locking git dependencies in the pylock.toml format. ([#3582](https://github.com/pdm-project/pdm/issues/3582))\n\n\n## Release v2.25.4 (2025-06-30)\n\n### Bug Fixes\n\n- Add credentials when passing source urls to uv resolver. ([#3553](https://github.com/pdm-project/pdm/issues/3553))\n- Redact credentials in source urls in the log output, and inject credentials into the source url for uv sync command as well. ([#3555](https://github.com/pdm-project/pdm/issues/3555))\n- Fix a bug that extra dependencies of transitive dependencies are not properly installed when USE_UV=true ([#3558](https://github.com/pdm-project/pdm/issues/3558))\n- Improve the terminal output when setting up a script environment. ([#3560](https://github.com/pdm-project/pdm/issues/3560))\n- Skip non-existent library paths in post-install steps when trying to fix the pth files. ([#3561](https://github.com/pdm-project/pdm/issues/3561))\n\n### Dependencies\n\n- Update `resolvelib` to 1.2.0. ([#3557](https://github.com/pdm-project/pdm/issues/3557))\n\n\n## Release v2.25.3 (2025-06-22)\n\n### Bug Fixes\n\n- Fix a bug that local file package metadata was missing when reading the lockfile. ([#3545](https://github.com/pdm-project/pdm/issues/3545))\n- Extract `dependency-groups` and `extras` markers from `marker` value when parsing pylock.toml. ([#3550](https://github.com/pdm-project/pdm/issues/3550))\n\n\n## Release v2.25.2 (2025-06-16)\n\nNo significant changes.\n\n\n## Release v2.25.1 (2025-06-14)\n\n### Bug Fixes\n\n- Fix duplicated dependencies added to the lock file when the same dependency with extras is requested. ([#3542](https://github.com/pdm-project/pdm/issues/3542))\n- Stabilize order of the `extras` and `dependency-groups` fields in pylock output. ([#3543](https://github.com/pdm-project/pdm/issues/3543))\n\n\n## Release v2.25.0 (2025-06-13)\n\n### Features & Improvements\n\n- Support pylock as alternative lock format and make it opt-in by config. ([#3481](https://github.com/pdm-project/pdm/issues/3481))\n- Search for package metadata in lock file first when reuse strategy is used. ([#3522](https://github.com/pdm-project/pdm/issues/3522))\n\n### Bug Fixes\n\n- Fix Windows 11 install pdm error, which is because of msgpack install failure. ([#3485](https://github.com/pdm-project/pdm/issues/3485))\n- Change the return type of `array_of_inline_tables` to list[dict] from list[str] ([#3523](https://github.com/pdm-project/pdm/issues/3523))\n- Ensure uv resolver to include hash for package files. ([#3531](https://github.com/pdm-project/pdm/issues/3531))\n- Avoid infinite recursion when reading pyproject.toml with circular file dependencies. ([#3539](https://github.com/pdm-project/pdm/issues/3539))\n\n\n## Release v2.24.2 (2025-05-23)\n\n### Bug Fixes\n\n- Reinstalling local wheel if its checksum changes. ([#3503](https://github.com/pdm-project/pdm/issues/3503))\n- Ignore HTTP cache entries if deserialization fails. ([#3515](https://github.com/pdm-project/pdm/issues/3515))\n- Fetch missing URLs when `static_urls` is not enabled when running `pdm export -f pylock`. ([#3517](https://github.com/pdm-project/pdm/issues/3517))\n- Missing self package when `--self` or `--editable-self` is passed to `pdm export -f pylock`. ([#3518](https://github.com/pdm-project/pdm/issues/3518))\n\n### Miscellany\n\n- Add Python 3.14 to the test matrix. ([#3506](https://github.com/pdm-project/pdm/issues/3506))\n\n\n## Release v2.24.1 (2025-04-23)\n\n### Bug Fixes\n\n- Install the project when using the `BaseSynchronizer` with `install_self` set\n  to `True`. This fixes the bug that when calling `pdm sync --quiet`, it skips\n  installing the project itself. ([#3484](https://github.com/pdm-project/pdm/issues/3484))\n- Mark one additional test as requiring network, and fix another one\n  not to require it anymore. ([#3487](https://github.com/pdm-project/pdm/issues/3487))\n\n\n## Release v2.24.0 (2025-04-18)\n\n### Features & Improvements\n\n- New command `pdm new` that behaves like `pdm init` but creates a new project. ([#3462](https://github.com/pdm-project/pdm/issues/3462))\n- Support use `--name` as project name for command `pdm new` e.g. `pdm new hello --name world` ([#3476](https://github.com/pdm-project/pdm/issues/3476))\n- Support exporting to pylock.toml format as described by PEP 751. ([#3480](https://github.com/pdm-project/pdm/issues/3480))\n\n### Bug Fixes\n\n- Pass the `--quiet` option to `pdm sync` command. ([#3401](https://github.com/pdm-project/pdm/issues/3401))\n- If a `.python-version` file is found and it contains multiple lines, the file will be ignored. The usage of the `.python-version` file can be disabled, if configuration value `python.use_python_version` (or environment variable `PDM_USE_PYTHON_VERSION`) is `False`. ([#3417](https://github.com/pdm-project/pdm/issues/3417))\n- fix `pdm config -e` command to open read-only file under linux ([#3423](https://github.com/pdm-project/pdm/issues/3423))\n- Replace project names and import names in both `README.md` and `pyproject.toml` when running `pdm init <template>`. ([#3460](https://github.com/pdm-project/pdm/issues/3460))\n- Fix a bug that URL dependency hashes are not updated if running `pdm lock --update-reuse`. ([#3461](https://github.com/pdm-project/pdm/issues/3461))\n\n\n## Release v2.23.1 (2025-04-09)\n\n### Features & Improvements\n\n- Use `pyapp` to wrap `pdm` as a Python application that bootstrap itself at runtime. ([#3429](https://github.com/pdm-project/pdm/issues/3429))\n- Support all providers `id` is supporting currently for OIDC trusted publishing ([#3441](https://github.com/pdm-project/pdm/issues/3441))\n\n### Bug Fixes\n\n- Installation error for local plugins specified with file URL without a name. ([#3407](https://github.com/pdm-project/pdm/issues/3407))\n- Eliminate the warning about inherit_metadata when using uv mode. ([#3434](https://github.com/pdm-project/pdm/issues/3434))\n- Fix an installation failure when installing editable local dependencies on Windows and Python 3.13. ([#3444](https://github.com/pdm-project/pdm/issues/3444))\n- Fix a bug that overridden requirements in lock file get rewritten when adding a new requirement. ([#3446](https://github.com/pdm-project/pdm/issues/3446))\n- Cyclic group inclusion is detected incorrectly. Also show the cyclic group names in the error message. ([#3447](https://github.com/pdm-project/pdm/issues/3447))\n- Fix a bug that `pdm remove` doesn't handle dependency groups include correctly. ([#3452](https://github.com/pdm-project/pdm/issues/3452))\n- Update `unearth` to address an issue downloading git repos with short commit hash. ([#3455](https://github.com/pdm-project/pdm/issues/3455))\n\n\n## Release v2.23.0 (2025-04-01)\n\n### Features & Improvements\n\n- Add `pdm python find` command to search for a python interpreter. ([#3389](https://github.com/pdm-project/pdm/issues/3389))\n- `pdm import` now converts `package-mode` from Poetry's settings table to `distribution`. ([#3427](https://github.com/pdm-project/pdm/issues/3427))\n\n### Bug Fixes\n\n- Excluding non-existing groups for `pdm remove`. ([#3404](https://github.com/pdm-project/pdm/issues/3404))\n- Fix a bug that `pdm add` and `pdm update` remove dependency groups incorrectly. ([#3418](https://github.com/pdm-project/pdm/issues/3418))\n- Fix a bug that using resolution overrides drops extra dependencies. ([#3426](https://github.com/pdm-project/pdm/issues/3426))\n\n\n## Release v2.22.4 (2025-03-07)\n\n### Bug Fixes\n\n- Ensure dev-dependencies are added to the correct group when the `tool.pdm.dev-dependencies` table has groups. ([#3392](https://github.com/pdm-project/pdm/issues/3392))\n\n\n## Release v2.22.3 (2025-01-27)\n\n### Bug Fixes\n\n- Don't validate local file requirements that are not used. ([#3376](https://github.com/pdm-project/pdm/issues/3376))\n- Don't set \"dependencies\" as empty list for uv toml if there is no dependencies in the raw toml file. ([#3378](https://github.com/pdm-project/pdm/issues/3378))\n- Add a dummy project name to the script environment pyproject.toml. ([#3382](https://github.com/pdm-project/pdm/issues/3382))\n\n\n## Release v2.22.2 (2025-01-11)\n\n\n### Features & Improvements\n\n- Write installer metadata like `INSTALLER` and `REQUESTED` to dist-info directory when installing packages. ([#3359](https://github.com/pdm-project/pdm/issues/3359))\n- Respect `.python-version` file in the project root directory when selecting the Python interpreter. By default, it will be written when running `pdm use` command. ([#3367](https://github.com/pdm-project/pdm/issues/3367))\n\n### Bug Fixes\n\n- Fix a problem of missing dependencies when adding to dev dependencies if both editable and non-editable dependencies exist. ([#3361](https://github.com/pdm-project/pdm/issues/3361))\n- Use stdlib for URL <-> Path conversions. ([#3362](https://github.com/pdm-project/pdm/issues/3362))\n- `shellingham.detect_shell()` returns `('tcsh', '/bin/tcsh')` for tcsh on FreeBSD, so the current code tries to use the Bash venv activation script and fails due to syntax error. This change fixes the issue. ([#3366](https://github.com/pdm-project/pdm/issues/3366))\n- Fix a performance issue because pypi source credentials were being queried many times from keyring. ([#3368](https://github.com/pdm-project/pdm/issues/3368))\n\n\n## Release v2.22.1 (2024-12-19)\n\n### Bug Fixes\n\n- Fix zsh hanging issue by removing PyPI package completion. ([#3329](https://github.com/pdm-project/pdm/issues/3329))\n- Write dev dependencies to `dependency-groups` section when importing project from other package managers. ([#3354](https://github.com/pdm-project/pdm/issues/3354))\n\n### Miscellany\n\n- Show a warning when resolving against cross-platform targets under uv mode. ([#3341](https://github.com/pdm-project/pdm/issues/3341))\n\n\n## Release v2.22.0 (2024-12-09)\n\n### Features & Improvements\n\n- Use minimal template if the project is an application. ([#3295](https://github.com/pdm-project/pdm/issues/3295))\n- Add one `safe_compatible` version specifiers saving strategy. ([#3301](https://github.com/pdm-project/pdm/issues/3301))\n- Allow customizing scripts display with `scripts.show_header` settings. ([#3313](https://github.com/pdm-project/pdm/issues/3313))\n- Speed up the resolution by only resolving wheel candidates if possible. ([#3319](https://github.com/pdm-project/pdm/issues/3319))\n- Drop version from the search result, following the change of warehouse. ([#3328](https://github.com/pdm-project/pdm/issues/3328))\n- Support `overrides` settings under `[tool.pdm.resolution]` with use_uv ([#3330](https://github.com/pdm-project/pdm/issues/3330))\n\n### Bug Fixes\n\n- No longer requires `wheel` to build a setuptools-backed package. ([#3320](https://github.com/pdm-project/pdm/issues/3320))\n- Fix an inconsistent behavior when running `pdm remove <package>` with uv enabled. ([#3323](https://github.com/pdm-project/pdm/issues/3323))\n- Fix: uninstallation error when pdm is not installed before. ([#3325](https://github.com/pdm-project/pdm/issues/3325))\n- Fix a bug in uv mode that direct URL dependencies can't be installed. ([#3332](https://github.com/pdm-project/pdm/issues/3332))\n- Fix a crash issue when rewriting dependency groups with `include-group` items. ([#3333](https://github.com/pdm-project/pdm/issues/3333))\n- Also read username from keyring if missing in source/repository config. ([#3334](https://github.com/pdm-project/pdm/issues/3334))\n- Allow configuring repositories in project. ([#3335](https://github.com/pdm-project/pdm/issues/3335))\n\n### Miscellany\n\n- Mark tests that require uv and skip them if uv is not found. ([#3324](https://github.com/pdm-project/pdm/issues/3324))\n\n\n## Release v2.21.0 (2024-11-25)\n\n### Features & Improvements\n\n- Pass original working directory as env variable to pdm scripts ([#3179](https://github.com/pdm-project/pdm/issues/3179))\n- Output similar commands or script command when the input command is not correct ([#3270](https://github.com/pdm-project/pdm/issues/3270))\n- improve readability of Python interpreter validation message ([#3276](https://github.com/pdm-project/pdm/issues/3276))\n- Print task name by default when using `pdm run` ([#3277](https://github.com/pdm-project/pdm/issues/3277))\n- Make `OrderedSet.__contains__` run in O(1) ([#3280](https://github.com/pdm-project/pdm/issues/3280))\n- Emit `post_lock` after writing pyproject.toml and pdm.lock in add/update ([#3285](https://github.com/pdm-project/pdm/issues/3285))\n- Drop support of Python 3.8 ([#3298](https://github.com/pdm-project/pdm/issues/3298))\n\n### Bug Fixes\n\n- Fix the name normalization issue for optional dependency groups. ([#3271](https://github.com/pdm-project/pdm/issues/3271))\n- Don't use uv when installing plugins in project. ([#3283](https://github.com/pdm-project/pdm/issues/3283))\n- Fix the bug that pdm plugins are invalid after installation on ubuntu system python. ([#3289](https://github.com/pdm-project/pdm/issues/3289))\n\n\n## Release v2.20.1 (2024-11-09)\n\n### Features & Improvements\n\n- Add a fixer to remove the deprecated `cross_platform` strategy from lock file. ([#3259](https://github.com/pdm-project/pdm/issues/3259))\n\n### Bug Fixes\n\n- Fix the bug that `pdm build` would fail when `use_uv` is true. ([#3231](https://github.com/pdm-project/pdm/issues/3231))\n- Fix group name normalization when comparing groups. ([#3247](https://github.com/pdm-project/pdm/issues/3247))\n- Inherit file descriptors instead of closing when running child processes in `pdm run`. ([#3252](https://github.com/pdm-project/pdm/issues/3252))\n- Fix using `no_proxy` when `all_proxy` is set. ([#3254](https://github.com/pdm-project/pdm/issues/3254))\n- Preserve multiline arrays and don't add empty tool.pdm table header when updating the pyproject.toml. ([#3258](https://github.com/pdm-project/pdm/issues/3258))\n- Fix compatibility of `ErrorArgumentParser` for Python 3.12 and above. ([#3264](https://github.com/pdm-project/pdm/issues/3264))\n\n\n## Release v2.20.0 (2024-10-31)\n\n### Features & Improvements\n\n- Support dependency groups as standardized by [PEP 735](https://peps.python.org/pep-0735/). By default, dev dependencies will be written to `[dependency-groups]` table. ([#3230](https://github.com/pdm-project/pdm/issues/3230))\n\n### Bug Fixes\n\n- Fix a bug that `strategy.inherit_metadata` config is not honored when using `--lockfile` option. ([#3232](https://github.com/pdm-project/pdm/issues/3232))\n- Always perform install-time resolution when `use_uv` is on. ([#3233](https://github.com/pdm-project/pdm/issues/3233))\n\n### Miscellany\n\n- Update `resolvelib` to 1.1.0. ([#3235](https://github.com/pdm-project/pdm/issues/3235))\n\n\n## Release v2.19.3 (2024-10-19)\n\n### Features & Improvements\n\n- Allow linking existing Python interpreters to PDM's managed location. ([#3215](https://github.com/pdm-project/pdm/issues/3215))\n\n### Bug Fixes\n\n- Fix a bug that overrides provided by environment variables do not work. ([#3182](https://github.com/pdm-project/pdm/issues/3182))\n- Allow prereleases when the requirement is pinned even if disabled by project ([#3202](https://github.com/pdm-project/pdm/issues/3202))\n- Pass the python path to the uv venv command. ([#3204](https://github.com/pdm-project/pdm/issues/3204))\n- Fix the infinite loop when running in uv mode if the current project has dynamic metadata. ([#3207](https://github.com/pdm-project/pdm/issues/3207))\n- Add `--no-frozen-deps` option to `install-pdm.py` script to allow installing newer versions of dependencies. ([#3213](https://github.com/pdm-project/pdm/issues/3213))\n- `pdm self update` now prefers the locked dependencies unless `--no-frozen-deps` is specified. ([#3216](https://github.com/pdm-project/pdm/issues/3216))\n- By default, `pdm outdated` will only list direct dependencies. This can be changed by adding the `--include-sub` option. ([#3218](https://github.com/pdm-project/pdm/issues/3218))\n\n### Documentation\n\n- Show users the way to uninstall pdm in a more obvious way ([#2470](https://github.com/pdm-project/pdm/issues/2470))\n\n\n## Release v2.19.2 (2024-10-11)\n\n\n### Features & Improvements\n\n- Support installing free-threaded Python interpreters with the `t` suffix. ([#3201](https://github.com/pdm-project/pdm/issues/3201))\n\n### Bug Fixes\n\n- `use_uv` fails to lock when there are non-ascii characters in pyproject.toml on Windows. ([#3181](https://github.com/pdm-project/pdm/issues/3181))\n- Fix the `pre_install` and `post_install` signals receiving an exhausted generator, instead of a list of packages. ([#3190](https://github.com/pdm-project/pdm/issues/3190))\n- Create backup file with random filename to avoid conflicts. ([#3193](https://github.com/pdm-project/pdm/issues/3193))\n- Fix the logic error in the `uv` format marker matching. ([#3197](https://github.com/pdm-project/pdm/issues/3197))\n- `pdm lock --check` on a lockfile generated with older PDM version has a 0 exit code when there's a change in `pyproject.toml`. ([#3199](https://github.com/pdm-project/pdm/issues/3199))\n\n### Documentation\n\n- Fixed *Bash Completion* suggestion so it doesn't require root privileges ([#3183](https://github.com/pdm-project/pdm/issues/3183))\n\n\n## Release v2.19.1 (2024-09-23)\n\n\n### Bug Fixes\n\n- PDM libraries are not loaded correctly for in-process scripts when installed in the user site. ([#3178](https://github.com/pdm-project/pdm/issues/3178))\n\n\n## Release v2.19.0 (2024-09-23)\n\n### Breaking Changes\n\n- The minimum supported Python version of projects using PDM has been bumped to 3.8. ([#3176](https://github.com/pdm-project/pdm/issues/3176))\n\n\n### Bug Fixes\n\n- Fallback version to 0.0.0 when the version is not specified or empty. This can avoid crash when building such project. ([#3163](https://github.com/pdm-project/pdm/issues/3163))\n- Ensures that  `/` is URL encoded in sources URL environment variables. ([#3169](https://github.com/pdm-project/pdm/issues/3169))\n- Call functions from shared library in the in-process `env_spec.py` script. ([#3176](https://github.com/pdm-project/pdm/issues/3176))\n\n### Removals and Deprecations\n\n- PDM no longer falls back to `setuptools-pep660` when the build backend doesn't support PEP 660. ([#3159](https://github.com/pdm-project/pdm/issues/3159))\n\n### Miscellany\n\n- Change the project structure to a normal package from a namespace package. ([#3155](https://github.com/pdm-project/pdm/issues/3155))\n\n\n## Release v2.18.2 (2024-09-10)\n\n### Bug Fixes\n\n- Respect the `excludes` and `overrides` settings when installing packages. ([#3113](https://github.com/pdm-project/pdm/issues/3113))\n- Fix a bug of export command that packages with extras are included twice. ([#3123](https://github.com/pdm-project/pdm/issues/3123))\n- Remove empty groups when removing packages with `pdm remove`. ([#3133](https://github.com/pdm-project/pdm/issues/3133))\n- When running `pdm venv purge`, if the current project's python version had been referencing the removed venv then clear it out. ([#3137](https://github.com/pdm-project/pdm/issues/3137))\n- Fix command `pdm config` to not show site configuration file path if it doesn't exist. ([#3149](https://github.com/pdm-project/pdm/issues/3149))\n- Now when `--no-markers` is used, the exported requirements can only work on the current platform. ([#3152](https://github.com/pdm-project/pdm/issues/3152))\n\n### Miscellany\n\n- Skip tests related to python installation on non-standard platforms. ([#3053](https://github.com/pdm-project/pdm/issues/3053))\n\n## Release v2.19.0a0 (2024-09-05)\n\n### Breaking Changes\n\n- `pre_install` and `post_install` signals now receive the list of packages to be installed, instead of a candidate mapping. ([#3144](https://github.com/pdm-project/pdm/issues/3144))\n\n### Features & Improvements\n\n- Deprecate `Core.synchronizer_class` attribute. To get the synchronizer class, use `Project.get_synchronizer` method instead.\n  Deprecate `Core.resolver_class` attribute. To get the resolver class, use `Project.get_resolver` method instead. ([#3144](https://github.com/pdm-project/pdm/issues/3144))\n- Add experimental support for `uv` as the resolver and installer. One can opt in by setting `use_uv` to `true` using `pdm config` command. ([#3144](https://github.com/pdm-project/pdm/issues/3144))\n\n\n## Release v2.18.1 (2024-08-16)\n\n\n### Bug Fixes\n\n- Skip checking `project.name` if it is absent when running `pdm outdated`. ([#3095](https://github.com/pdm-project/pdm/issues/3095))\n- Don't remove the `cross_platform` strategy from old lock files. ([#3105](https://github.com/pdm-project/pdm/issues/3105))\n- Fix a bug that the VCS revision is lost if the candidate metadata is cached during resolution. ([#3107](https://github.com/pdm-project/pdm/issues/3107))\n- Fix a bug that PDM can't delete source password when saved in keyring. ([#3108](https://github.com/pdm-project/pdm/issues/3108))\n\n\n## Release v2.18.0 (2024-08-14)\n\n\n### Features & Improvements\n\n- Respect certificates in env vars `REQUESTS_CA_BUNDLE` and `CURL_CA_BUNDLE` when verifying SSL certificates. ([#3076](https://github.com/pdm-project/pdm/issues/3076))\n- Allow pypi.verify_ssl to be configured via PDM_PYPI_VERIFY_SSL environmental variable. ([#3081](https://github.com/pdm-project/pdm/issues/3081))\n- Clean logs older than 7 days. ([#3091](https://github.com/pdm-project/pdm/issues/3091))\n- Polish the UI looking of locking packages to display the progress. ([#3100](https://github.com/pdm-project/pdm/issues/3100))\n\n### Bug Fixes\n\n- Fixed `pdm venv activate` to remove quotes such that `iex (pdm venv activate)` works correctly ([#2895](https://github.com/pdm-project/pdm/issues/2895))\n- Don't crash if the version can't be resolved from the self project. ([#3077](https://github.com/pdm-project/pdm/issues/3077))\n- Don't fail `install-pdm.py` if there is an invalid `pyproject.toml` file under the current directory. ([#3085](https://github.com/pdm-project/pdm/issues/3085))\n- Make it able to expand env vars in the the dotenv file. Expose `PDM_PROJECT_ROOT` to the dotenv file for expansion. ([#3087](https://github.com/pdm-project/pdm/issues/3087))\n- Fix a bug that Python markers from the existing locked packages are considered when locking with `--append` option. ([#3089](https://github.com/pdm-project/pdm/issues/3089))\n- Backfill urls from configured indexed when exporting to requirements.txt. ([#3094](https://github.com/pdm-project/pdm/issues/3094))\n- Consider the auto-selected Python range when installing from requirements.txt. ([#3095](https://github.com/pdm-project/pdm/issues/3095))\n- Fix a bug that env vars do not override project config correctly. ([#3099](https://github.com/pdm-project/pdm/issues/3099))\n\n\n## Release v2.17.3 (2024-08-01)\n\n\n### Bug Fixes\n\n- Fix a crash issue when `requires-python` is absent in the project metadata. ([#3062](https://github.com/pdm-project/pdm/issues/3062))\n- Now correctly sets related config for PDM_IGNORE_SAVED_PYTHON when it is set to \"false\", \"no\", \"0\". ([#3064](https://github.com/pdm-project/pdm/issues/3064))\n- Fix a bug that PDM plugins installed from project-root cannot be loaded, if they have dependencies. ([#3067](https://github.com/pdm-project/pdm/issues/3067))\n\n\n## Release v2.17.2 (2024-07-31)\n\n\n### Features & Improvements\n\n- Improve the installation progress output to show the time elapsed. ([#3051](https://github.com/pdm-project/pdm/issues/3051))\n- The effect of `pypi.ignore_stored_index` changes a bit. Now even if it is true, index configurations in the config will still be loaded if the index is listed in the `pyproject.toml`. ([#3052](https://github.com/pdm-project/pdm/issues/3052))\n\n### Bug Fixes\n\n- Ignore invalid requires-python values from index. ([#3038](https://github.com/pdm-project/pdm/issues/3038))\n- Fix the group selection logic, to make `--without GROUP` work as expected. ([#3045](https://github.com/pdm-project/pdm/issues/3045))\n- Suppress outputs for `pdm python install --quiet`. ([#3049](https://github.com/pdm-project/pdm/issues/3049))\n\n\n## Release v2.17.1 (2024-07-19)\n\n\n### Bug Fixes\n\n- Raise dep-logic lower bound to 0.4.2 to fix issues with pdm lock after upgrading from older pdm versions ([#3033](https://github.com/pdm-project/pdm/issues/3033))\n- Correct the current platform and architecture for win32 and macos systems. ([#3035](https://github.com/pdm-project/pdm/issues/3035))\n\n### Miscellany\n\n- Fix zsh completions ([#3031](https://github.com/pdm-project/pdm/issues/3031))\n\n\n## Release v2.17.0 (2024-07-18)\n\n\n### Breaking Changes\n\n- `LockedRepository.all_candidates` now returns a `dict[str, list[Candidate]]` instead of `dict[str, Candidate]`. ([#2995](https://github.com/pdm-project/pdm/issues/2995))\n- `post_lock` hook now receives a resolution result of type `dict[str, list[Candidate]]`, instead of `dict[str, Candidate]`. ([#2995](https://github.com/pdm-project/pdm/issues/2995))\n\n### Features & Improvements\n\n- Support reading requirement constraints from pip-style requirement files for \"overriding\" via `--override` option. ([#2896](https://github.com/pdm-project/pdm/issues/2896))\n- Add a `--non-interactive` option for automation scenarios, also interactive prompts will not show up when not running in an interactive terminal. ([#2934](https://github.com/pdm-project/pdm/issues/2934))\n- Refactored `pdm python install --list` to reuse the same implementation as other cli commands that work with Python interpreters from pbs_installer. ([#2977](https://github.com/pdm-project/pdm/issues/2977))\n- Add `--license` and `--project-version` as CLI options to control and streamline them during `pdm init` - especially in automated scenarios with `--non-interactive` ([#2978](https://github.com/pdm-project/pdm/issues/2978))\n- Run pdm sync in \"post-rewrite\" stage of pre-commit ([#2994](https://github.com/pdm-project/pdm/issues/2994))\n- `Project.get_dependencies()` now returns a list of `Requirement` instead of a mapping.\n  The first argument of `Project.add_dependencies()` now accepts a list of `Requirement` instead of a mapping.\n  The old usage will be kept working for a short period of time and will be removed in the future. ([#2995](https://github.com/pdm-project/pdm/issues/2995))\n- Support locking for specific target, which is a combination of (python, platform, implementation) triple. Bump lock file version to `4.5.0`.\n\n  Example usage: `pdm lock --platform=linux --python=\"==3.8.*\" --implementation=cpython`. See the [docs](https://pdm-project.org/en/latest/usage/lock-targets) for more details. ([#2995](https://github.com/pdm-project/pdm/issues/2995))\n- Rename `--reuse-env` to `--recreate` for `run` command, and reverse the behavior. ([#2999](https://github.com/pdm-project/pdm/issues/2999))\n- PDM is now published with optional pinned dependencies using the pdm plugin [pdm-build-locked](https://pdm-build-locked.readthedocs.io/).\n\n  To install pdm with its dependencies pinned to the versions it was tested with, run:\n\n  ```bash\n\n      pipx install pdm[locked]\n  ```\n\n  To install optional dependency group copier:\n\n  ```bash\n\n      pipx install pdm[locked,copier-locked]\n  ```\n\n  This feature is entirely optional. Installing pdm without the extra will work the same way as before this change. ([#3001](https://github.com/pdm-project/pdm/issues/3001))\n- Added `--clean-unselected` alias for `--only-keep` ([#3007](https://github.com/pdm-project/pdm/issues/3007))\n- Group options for update strategy and save strategy. ([#3016](https://github.com/pdm-project/pdm/issues/3016))\n\n### Bug Fixes\n\n- When locking dependencies that references the self project, the referenced groups should also be recorded in the lockfile. ([#2976](https://github.com/pdm-project/pdm/issues/2976))\n- Retry failed installation jobs if they are run sequentially, such as for editable dependencies. ([#3005](https://github.com/pdm-project/pdm/issues/3005))\n- Fix the local path issue when `-p` is passed to change the project root. ([#3009](https://github.com/pdm-project/pdm/issues/3009))\n- Fix a bug that PDM can't install editable self package with non-isolated build in one go. ([#3018](https://github.com/pdm-project/pdm/issues/3018))\n- Add context when parsing version failed. ([#3020](https://github.com/pdm-project/pdm/issues/3020))\n- Fix a mistake in build env setup that will cause the `PATH` env var length to grow. ([#3022](https://github.com/pdm-project/pdm/issues/3022))\n\n### Removals and Deprecations\n\n- Remove the deprecation warning of `BaseCommand.__init__()` method. Now it doesn't take any arguments. ([#2995](https://github.com/pdm-project/pdm/issues/2995))\n- `Provider.get_reuse_candidate()` method is deprecated in favor of `Provider.iter_reuse_candidates()`, to return an iterable of reuse candidates. ([#2995](https://github.com/pdm-project/pdm/issues/2995))\n- `--no-markers` option in `pdm export` command becomes a no-op and is marked as deprecated, because it doesn't make sense anymore. ([#2995](https://github.com/pdm-project/pdm/issues/2995))\n- `ignore_compatibility` parameter of `Project.get_provider()`/`Project.get_repository()`/`Environment.get_finder()` is deprecated. Pass in a `EnvSpec` via `env_spec` parameter instead.\n  `requires_python` parameter of `pdm.resolver.core.resolve()` function is deprecated and has no effect.\n  `cross_platform` parameter of `pdm.cli.actions.resolve_candidates_from_lockfile()` function is deprecated and has no effect. ([#2995](https://github.com/pdm-project/pdm/issues/2995))\n\n\n## Release v2.16.1 (2024-06-26)\n\n\n### Bug Fixes\n\n- Fix new interface from pbs_installer regarding `build_dir` and best match auto-install strategy for `pdm use`\n  (same as for `pdm python install --list`) ([#2943](https://github.com/pdm-project/pdm/issues/2943))\n- Fix crash when pdm is used with `importlib-metadata` version 8.0. ([#2974](https://github.com/pdm-project/pdm/issues/2974))\n\n\n## Release v2.16.0 (2024-06-25)\n\n\n### Features & Improvements\n\n- Add `--no-extras` to `pdm export` to strip extras from the requirements. Now the default behavior is to keep extras. ([#2519](https://github.com/pdm-project/pdm/issues/2519))\n- Support PEP 723: running scripts with inline metadata in standalone environment with dependencies. ([#2924](https://github.com/pdm-project/pdm/issues/2924))\n- `pdm use` and `pdm python install` now take `requires-python` into account (incl. from pyproject.toml) if python version\n  not specified and `pdm use` provides auto installation by that. ([#2943](https://github.com/pdm-project/pdm/issues/2943))\n- `--no-isolation` no longer installs `build-requires` nor dynamic build dependencies, to be consistent with `pip`. ([#2944](https://github.com/pdm-project/pdm/issues/2944))\n- Add notifiers in CLI output when global project is being used. ([#2952](https://github.com/pdm-project/pdm/issues/2952))\n- Use `tool.pdm.resolution` table when calculating the content hash of project file, previously only `overrides` table was used.\n  This will change the hash already stored in the lockfile, so bump the lockfile version to `4.4.2`. ([#2956](https://github.com/pdm-project/pdm/issues/2956))\n\n### Bug Fixes\n\n- Add max retries on read timeout or bad connection. ([#2914](https://github.com/pdm-project/pdm/issues/2914))\n- Don't update local files if they don't change. ([#2966](https://github.com/pdm-project/pdm/issues/2966))\n- Don't list python versions that don't have any installation link for the current platform. ([#2970](https://github.com/pdm-project/pdm/issues/2970))\n\n### Documentation\n\n- Clarify the purposes of `pdm outdated` and `--unconstrained` option. ([#2965](https://github.com/pdm-project/pdm/issues/2965))\n- Some clarifications on the interpreter selection and central package cache. ([#2967](https://github.com/pdm-project/pdm/issues/2967))\n\n\n## Release v2.15.4 (2024-05-30)\n\n\n### Bug Fixes\n\n- Build wheel from sdist if available, to make sure sdist is built properly. This behavior is consistent with [pypa/build](https://pypi.org/project/build). ([#2843](https://github.com/pdm-project/pdm/issues/2843))\n- Fix the issue of self-referencing extra dependencies failing to be resolved for local packages. ([#2898](https://github.com/pdm-project/pdm/issues/2898))\n- Fix an issue of max recursion depth error when parsing a poetry project with circular dependencies on local packages. ([#2900](https://github.com/pdm-project/pdm/issues/2900))\n- Fix a bug that VCS dependencies and `--self` don't work in the exported requirements.txt with hashes. ([#2908](https://github.com/pdm-project/pdm/issues/2908))\n- Fix a cache miss when there exist built wheels for a given link. ([#2912](https://github.com/pdm-project/pdm/issues/2912))\n- Don't try to store caches when `--no-cache` is given. ([#2913](https://github.com/pdm-project/pdm/issues/2913))\n\n\n## Release v2.15.3 (2024-05-20)\n\n\n### Bug Fixes\n\n- Fixed pdm venv activate, to also work for windows. And added documentation on how to authenticate to Azure Artifacts ([#2851](https://github.com/pdm-project/pdm/issues/2851))\n- Don't show unsupported formats in `pdm export`. ([#2877](https://github.com/pdm-project/pdm/issues/2877))\n- Proxy (`HTTP_PROXY` env vars) settings are ignored for custom indexes. ([#2880](https://github.com/pdm-project/pdm/issues/2880))\n- Fix the quoting of venv activate command for powershell. ([#2881](https://github.com/pdm-project/pdm/issues/2881))\n- Raise an error if the package given by `pdm update` does not exist in the select dependency group but in other groups. ([#2885](https://github.com/pdm-project/pdm/issues/2885))\n\n\n## Release v2.15.2 (2024-05-08)\n\n\n### Features & Improvements\n\n- Use `get_runner()` method to build the task runner in `run` command. `runner_cls` attribute is deprecated. ([#2872](https://github.com/pdm-project/pdm/issues/2872))\n\n### Bug Fixes\n\n- Expand `${PROJECT_ROOT}` in source URLs. ([#2846](https://github.com/pdm-project/pdm/issues/2846))\n- Fix env and other options being inherited in nested composite scripts. ([#2849](https://github.com/pdm-project/pdm/issues/2849))\n- Keep the `${PROJECT_ROOT}` variable in dependencies after running `pdm lock --update-reuse`. ([#2852](https://github.com/pdm-project/pdm/issues/2852))\n- Make `direct_minimal_versions` work on newly added dependencies. ([#2853](https://github.com/pdm-project/pdm/issues/2853))\n- Fix a syntax error in the zsh completion script. ([#2868](https://github.com/pdm-project/pdm/issues/2868))\n\n\n## Release v2.15.1 (2024-04-25)\n\n\n### Bug Fixes\n\n- Disable check update in `zsh` completion script. ([#2838](https://github.com/pdm-project/pdm/issues/2838))\n- Fixes cached packages metadata files (`.referrers`) collisions on `sync` when using a `venv` with `symlink` cache method. ([#2839](https://github.com/pdm-project/pdm/issues/2839))\n\n### Documentation\n\n- Build docs with object inventory to support cross references from Sphinx documentation projects. ([#2841](https://github.com/pdm-project/pdm/issues/2841))\n\n\n## Release v2.15.0 (2024-04-19)\n\n\n### Features & Improvements\n\n- Packages format preferences can now be defined in the project `pyproject.toml`\n  using the `no-binary`, `only-binary` and `prefer-binary` keys of the `tool.pdm.resolution` section. ([#2656](https://github.com/pdm-project/pdm/issues/2656))\n\n### Bug Fixes\n\n- Don't create project and virtualenv when running `pdm python install`. ([#2809](https://github.com/pdm-project/pdm/issues/2809))\n- Clean up the python installation directory if a previous download was unsuccessful. ([#2810](https://github.com/pdm-project/pdm/issues/2810))\n- Don't cache editable installations. ([#2816](https://github.com/pdm-project/pdm/issues/2816))\n- Fix a bug that installing in-project plugins with editable local paths doesn't work. ([#2820](https://github.com/pdm-project/pdm/issues/2820))\n- Don't create log directory until it's needed, to fix a PermissionError in docker environment. ([#2825](https://github.com/pdm-project/pdm/issues/2825))\n- Fix recursive script detection on multiple invocations. ([#2829](https://github.com/pdm-project/pdm/issues/2829))\n\n\n## Release v2.14.0 (2024-04-12)\n\n\n### Features & Improvements\n\n- Revert the package cache introduced in 2.13. Don't cache the decompressed contents of wheels unless being told so. ([#2803](https://github.com/pdm-project/pdm/issues/2803))\n\n### Bug Fixes\n\n- Fix inconsistent logging when `pdm use` a different python interpreter ([#2776](https://github.com/pdm-project/pdm/issues/2776))\n- Fix PDM unable to find Python interpreters when `PDM_IGNORE_ACTIVE_VENV` is set ([#2779](https://github.com/pdm-project/pdm/issues/2779))\n- Check verify_ssl when trusting each source. ([#2784](https://github.com/pdm-project/pdm/issues/2784))\n- Fix name check for project itself in `pdm outdated` ([#2785](https://github.com/pdm-project/pdm/issues/2785))\n- Fix a regression that proxy env vars are not respected. ([#2788](https://github.com/pdm-project/pdm/issues/2788))\n- Fix an issue that venv provider can't be found when providers are explicitly configured. ([#2792](https://github.com/pdm-project/pdm/issues/2792))\n- Fix a bug that `[tool.pdm.options]` are ignored if `-c/--config CONFIG` is given. ([#2793](https://github.com/pdm-project/pdm/issues/2793))\n- Make `--without` respect groups in `dev-dependencies` ([#2799](https://github.com/pdm-project/pdm/issues/2799))\n\n\n## Release v2.13.3 (2024-04-08)\n\n### Bug Fixes\n\n- Per-source configuration for ca-certs and client-cert. [#2754](https://github.com/pdm-project/pdm/issues/2754)\n- Remove all caches by removing individual cache types one by one. [#2757](https://github.com/pdm-project/pdm/issues/2757)\n- Use the default HTTP client when downloading the pythons, to use the certificates settings. [#2759](https://github.com/pdm-project/pdm/issues/2759)\n- Fix a race condition where pth files take effect when multiple packages are installed in parallel. [#2762](https://github.com/pdm-project/pdm/issues/2762)\n- Refuse to run recursive composite scripts. [#2766](https://github.com/pdm-project/pdm/issues/2766)\n\n## Release v2.13.2 (2024-03-30)\n\n### Bug Fixes\n\n- Fix errors when parsing poetry format that contains special characters in author name.\n  Poetry-specific `parse_name_email` and `NAME_EMAIL_RE` moved from `pdm.formats.base` to `pdm.formats.poetry`. [#2665](https://github.com/pdm-project/pdm/issues/2665)\n- Fix a race condition in cached packages. When a cached package is being created it shouldn't be used for installation. [#2739](https://github.com/pdm-project/pdm/issues/2739)\n- Add back `PreparedCandidate.build()` for backward-compatibility. [#2747](https://github.com/pdm-project/pdm/issues/2747)\n\n### Documentation\n\n- Fixed a small non-code typo in docs and provided better wording. [#2740](https://github.com/pdm-project/pdm/issues/2740)\n\n## Release v2.13.1 (2024-03-29)\n\n### Bug Fixes\n\n- Fix a bug that PDM couldn't find interpreters for global project. [#2726](https://github.com/pdm-project/pdm/issues/2726)\n- Make the cache package path shorter to solve the Windows path problem. [#2730](https://github.com/pdm-project/pdm/issues/2730)\n\n### Documentation\n\n- Extract \"Lock file\" doc from \"Manage Dependencies\" doc. [#2725](https://github.com/pdm-project/pdm/issues/2725)\n\n## Release v2.13.0 (2024-03-27)\n\n### Features & Improvements\n\n- Add option to exclude group(s) when running ```pdm sync/install -G:all``` by adding flag ```--without group1,group2,...``` [#2258](https://github.com/pdm-project/pdm/issues/2258)\n- Default to log to user home and make logs directory configurable. [#2398](https://github.com/pdm-project/pdm/issues/2398)\n- Add an option `keep_going` to continue on errors for composite scripts and return the last failing exit code. [#2582](https://github.com/pdm-project/pdm/issues/2582)\n- Add an option `working_dir` for PDM's scripts to set the current working directory. [#2620](https://github.com/pdm-project/pdm/issues/2620)\n- Allow updating specific sub-dependencies (i.e., transitive dependencies) in the lock file. [#2628](https://github.com/pdm-project/pdm/issues/2628)\n- Add `--config-setting` option to `add/install/sync/update/remove/export` commands, the config settings dictionary will be shared by all packages. [#2636](https://github.com/pdm-project/pdm/issues/2636)\n- Cache the decompressed contents of wheels for faster access. [#2660](https://github.com/pdm-project/pdm/issues/2660)\n- Add configuration for timeout for network requests. [#2680](https://github.com/pdm-project/pdm/issues/2680)\n- Reuse the request session within the environment. [#2697](https://github.com/pdm-project/pdm/issues/2697)\n- Caches can be disabled by using the `--no-cache` option or setting the `PDM_NO_CACHE` environment variable. [#2702](https://github.com/pdm-project/pdm/issues/2702)\n- Switch to `httpx.Client` for HTTP requests, drop `requests` dependency. [#2709](https://github.com/pdm-project/pdm/issues/2709)\n- We have timemachine now! You can exclude packages published newer than a certain date via `pdm lock --exclude-newer=<date>`, allowing reproduction of resolutions regardless of new package releases. [#2712](https://github.com/pdm-project/pdm/issues/2712)\n- Add command `pdm outdated` to check the outdated packages and list the latest versions. [#2718](https://github.com/pdm-project/pdm/issues/2718)\n- When `python.use_venv` is on, always try to create a virtualenv when using `pdm use` to switch the Python interpreter. [#2720](https://github.com/pdm-project/pdm/issues/2720)\n- Support installing Pythons from [python-build-standalone](https://github.com/indygreg/python-build-standalone). Add command group `pdm python` to manage Python installations. And `pdm use` can automatically install the Python interpreter if it's not found. [#2721](https://github.com/pdm-project/pdm/issues/2721)\n- Supports custom distribution files path via `-d/--dest` option for `pdm publish`. [#2723](https://github.com/pdm-project/pdm/issues/2723)\n\n### Bug Fixes\n\n- Don't modify TOML tables that are not related to PDM. [#2666](https://github.com/pdm-project/pdm/issues/2666)\n- Made `--without` imply `--with :all`. [#2670](https://github.com/pdm-project/pdm/issues/2670)\n- Expand user path for `venv.location` and other path-like config values. [#2672](https://github.com/pdm-project/pdm/issues/2672)\n- Give a default version when it's missing in `pyproject.toml` when parsing candidate's metadata. [#2677](https://github.com/pdm-project/pdm/issues/2677)\n- Fix the issue that ANSI codes are shown in the output of `pdm --help` on Windows. [#2678](https://github.com/pdm-project/pdm/issues/2678)\n- Don't show empty configuration sections in `pdm config`. [#2683](https://github.com/pdm-project/pdm/issues/2683)\n\n### Documentation\n\n- Document the difference between `[tool.pdm.scripts]` and `[project.scripts]` [#2121](https://github.com/pdm-project/pdm/issues/2121)\n\n### Removals and Deprecations\n\n- Remove the support of `pth` cache method. And `symlink` cache method now behaves the same as `symlink_individual` cache method. [#2660](https://github.com/pdm-project/pdm/issues/2660)\n- Remove `pdm.models.environment` module deprecated before. Also remove the renamed members from `pdm.environments`. [#2710](https://github.com/pdm-project/pdm/issues/2710)\n\n### Miscellany\n\n- Delete `setup.cfg`, move tool configurations under it to `pyproject.toml` [#2703](https://github.com/pdm-project/pdm/issues/2703)\n\n\nRelease v2.12.4 (2024-02-26)\n----------------------------\n\n### Features & Improvements\n\n- Use env PDM_NO_EDITABLE as the default value for --no-editable option. [#2613](https://github.com/pdm-project/pdm/issues/2613)\n\n### Bug Fixes\n\n- Reset project.environment when importing from setup.py, to fix resolution error. [#2608](https://github.com/pdm-project/pdm/issues/2608)\n- Do not fetch package hashes when `--frozen-lockfile` is passed. [#2630](https://github.com/pdm-project/pdm/issues/2630)\n- Make sure non-venv interpreters are used by venv creator. [#2631](https://github.com/pdm-project/pdm/issues/2631)\n- Don't cause a hard failure if the local directory doesn't exist. [#2650](https://github.com/pdm-project/pdm/issues/2650)\n\n### Documentation\n\n- Fix the default value for negative CLI flags. [#2642](https://github.com/pdm-project/pdm/issues/2642)\n- Auto-gen configuration reference documentation. [#2645](https://github.com/pdm-project/pdm/issues/2645)\n\n\nRelease v2.12.3 (2024-02-01)\n----------------------------\n\n### Bug Fixes\n\n- fix the package-type fixer won't update toml properly for \"Nested Section Ordering Issue in TOML\". [#2578](https://github.com/pdm-project/pdm/issues/2578)\n- Unable to force override a package if the package is required with extras. [#2586](https://github.com/pdm-project/pdm/issues/2586)\n- Failed to clone template repository if the URL contains the rev part. [#2597](https://github.com/pdm-project/pdm/issues/2597)\n- Handle legacy specifiers when converting from poetry project. [#2599](https://github.com/pdm-project/pdm/issues/2599)\n\n### Documentation\n\n- Fix typo in template docs [#2588](https://github.com/pdm-project/pdm/issues/2588)\n\n\nRelease v2.12.2 (2024-01-21)\n----------------------------\n\n### Bug Fixes\n\n- Fix the auto fixer for package-type. [#2564](https://github.com/pdm-project/pdm/issues/2564)\n- Fix the wrong installation destination for header files when installing build requirements. [#2573](https://github.com/pdm-project/pdm/issues/2573)\n- Install header files into package namespace under `include` directory. [#2574](https://github.com/pdm-project/pdm/issues/2574)\n\n\nRelease v2.12.1 (2024-01-17)\n----------------------------\n\n### Bug Fixes\n\n- Hotfix: missing `identifier` attribute for package type fixer. [#2564](https://github.com/pdm-project/pdm/issues/2564)\n\n\nRelease v2.12.0 (2024-01-17)\n----------------------------\n\n### Features & Improvements\n\n- Allow excluding packages from the lockfile via `tool.pdm.resolution.excludes` setting, the dependencies will also be skipped. [#1316](https://github.com/pdm-project/pdm/issues/1316)\n- Rename `--no-lock` option to `--frozen-lockfile`. [#2496](https://github.com/pdm-project/pdm/issues/2496)\n- Add `--no-hashes` as the recommended option name in favor of `--without-hashes` for `pdm export` command. [#2497](https://github.com/pdm-project/pdm/issues/2497)\n- Add `--no-markers` to `export` command to exclude markers from the output. [#2497](https://github.com/pdm-project/pdm/issues/2497)\n- Allow initializing a project without extra project files, with a new builtin template \"minimal\". Run it with `pdm init minimal`. [#2543](https://github.com/pdm-project/pdm/issues/2543)\n- Change the warning category emitted by `deprecated_warning()` to `PDMDeprecationWarning`. [#2547](https://github.com/pdm-project/pdm/issues/2547)\n- Prereleases will be allowed if a prerelease version is pinned in the lockfile. This can be disabled by passing `--stable` option. [#2552](https://github.com/pdm-project/pdm/issues/2552)\n- Change `tracked_names` argument to keyword-only. Move `allow_prereleases` setting to `tool.pdm.resolution` table. [#2552](https://github.com/pdm-project/pdm/issues/2552)\n- Rename the `preferred_pins` argument of provider classes to `locked_candidates`, and deprecate the old name. [#2552](https://github.com/pdm-project/pdm/issues/2552)\n- Rename the `package-type` field under `tool.pdm` settings table to `distribution` to make it more clear. [#2564](https://github.com/pdm-project/pdm/issues/2564)\n\n### Bug Fixes\n\n- `tool.pdm.resolution` settings won't be honored when installing dependencies into the build environment. [#1316](https://github.com/pdm-project/pdm/issues/1316)\n- Fixed pdm list output containing full license text in some cases [#2538](https://github.com/pdm-project/pdm/issues/2538)\n- Fix the environment variable substitution for `cmd` scripts. [#2542](https://github.com/pdm-project/pdm/issues/2542)\n- Allow normal extension modules in wheel tags when the python is debug build. [#2548](https://github.com/pdm-project/pdm/issues/2548)\n- Don't use pypi.org when pypi.url is set. [#2560](https://github.com/pdm-project/pdm/issues/2560)\n\n### Removals and Deprecations\n\n- Remove deprecated methods from `Project`. Remove deprecated helper functions from `actions.py`. [#2547](https://github.com/pdm-project/pdm/issues/2547)\n\n\nRelease v2.11.2 (2024-01-02)\n----------------------------\n\n### Bug Fixes\n\n- Fix a KeyError raised when resolving a URL dependency without package name given. [#2488](https://github.com/pdm-project/pdm/issues/2488)\n- `pdm update --update-eager` can hit InconsistentCandidate error when dependency is included both through default dependencies and extra. [#2495](https://github.com/pdm-project/pdm/issues/2495)\n- `pdm install` should not warn when overwriting its own symlinks on `install`/`update`. [#2502](https://github.com/pdm-project/pdm/issues/2502)\n- Fix a bug that candidates without local version are rejected when the local version is pinned. [#2507](https://github.com/pdm-project/pdm/issues/2507)\n\n### Documentation\n\n- Add maturin as a compatible build backend in the docs. [#2510](https://github.com/pdm-project/pdm/issues/2510)\n\n\nRelease v2.11.1 (2023-12-14)\n----------------------------\n\n### Bug Fixes\n\n- Update candidate names before resolving markers, to fix a KeyError when the requirement is not named. [#2488](https://github.com/pdm-project/pdm/issues/2488)\n- Fix a KeyError when resolving packages that have parents that are no longer needed. [#2489](https://github.com/pdm-project/pdm/issues/2489)\n\n\nRelease v2.11.0 (2023-12-14)\n----------------------------\n\n### Features & Improvements\n\n- Officially drop the support for Python 3.7.\n- Allow exporting current project as editable dependency with `pdm export`. [#1910](https://github.com/pdm-project/pdm/issues/1910)\n- Improve the lockfile compatibility checking by using 3-digit version numbers. This can distinguish forward-compatibility and backward-compatibility. [#2164](https://github.com/pdm-project/pdm/issues/2164)\n- Add `--skip-existing` to `pdm publish` to ignore the uploading error if the package already exists. [#2362](https://github.com/pdm-project/pdm/issues/2362)\n- Use `==major.minor.*` as default requires python for application projects. [#2382](https://github.com/pdm-project/pdm/issues/2382)\n- We now use the `package-type` field in the `tool.pdm` table to differentiate between library and application projects. [#2394](https://github.com/pdm-project/pdm/issues/2394)\n- Add support for {pdm} placeholder in script definitions to call the same PDM entrypoint [#2408](https://github.com/pdm-project/pdm/issues/2408)\n- When exporting requirements, record the environment markers from all parents for each requirement. This allows the exported requirements to work on different platforms and Python versions. [#2418](https://github.com/pdm-project/pdm/issues/2418)\n- `pdm lock` now supports `--update-reuse` option to keep the pinned versions in the lockfile if possible. [#2419](https://github.com/pdm-project/pdm/issues/2419)\n- Introduce a new lock strategy `inherit_metadata` to inherit and merge markers from parent requirements. This is enabled by default when creating a new lockfile. [#2421](https://github.com/pdm-project/pdm/issues/2421)\n- New cache methods: `symlink_individual` for creating a symlink for each individual package file and `hardlink` for creating hardlinks. [#2425](https://github.com/pdm-project/pdm/issues/2425)\n- Add \"pdm sync\" pre-commit hook [#2474](https://github.com/pdm-project/pdm/issues/2474)\n- New update strategy: `reuse-installed`. When this strategy is enabled, PDM will try to reuse the versions already installed in the environment, even if the package names are given in the command line following `add` or `update`. This strategy is supported by `add`, `update` and `lock` commands. [#2479](https://github.com/pdm-project/pdm/issues/2479)\n- Show subcommand's help info when passing unrecognized arguments. [#2480](https://github.com/pdm-project/pdm/issues/2480)\n- add `PDM_CACHE_DIR` environment variable to configure cache directory location. [#2485](https://github.com/pdm-project/pdm/issues/2485)\n\n### Bug Fixes\n\n- Use the same order of Python interpreters as interactive mode in `pdm init -n`. [#2436](https://github.com/pdm-project/pdm/issues/2436)\n- `pdm init` now implies `--lib` if `--backend` is passed. [#2437](https://github.com/pdm-project/pdm/issues/2437)\n- Fix a bug that link collection ignores package-index-binding. [#2442](https://github.com/pdm-project/pdm/issues/2442)\n- Fix the wrong installation candidates for different architectures on Windows. [#2464](https://github.com/pdm-project/pdm/issues/2464)\n- Fix installing PEP 561 stub-only packages with `install.cache_method = \"symlink\"`. [#2466](https://github.com/pdm-project/pdm/issues/2466)\n- Fix a `KeyError` raised by `pdm update --unconstrained` when the project itself is listed as a dependency. [#2483](https://github.com/pdm-project/pdm/issues/2483)\n\n\nRelease v2.10.4 (2023-11-24)\n----------------------------\n\n### Bug Fixes\n\n- Do not detect as requirements.txt if the file is a python script. [#2416](https://github.com/pdm-project/pdm/issues/2416)\n- Provide information of the original line when parsing requirement fails. [#2417](https://github.com/pdm-project/pdm/issues/2417)\n- Resolve `-r` requirements paths relative to the requirement file they are specified in [#2422](https://github.com/pdm-project/pdm/issues/2422)\n- Updating package now overwrites the old files instead of removing before installing. [#2423](https://github.com/pdm-project/pdm/issues/2423)\n\n\nRelease v2.10.3 (2023-11-16)\n----------------------------\n\n### Bug Fixes\n\n- Create virtualenv for conda base Python. [#2409](https://github.com/pdm-project/pdm/issues/2409)\n\n\nRelease v2.10.2 (2023-11-16)\n----------------------------\n\n### Features & Improvements\n\n- Log the response text when `pdm publish` fails with HTTP error. [#2400](https://github.com/pdm-project/pdm/issues/2400)\n\n### Bug Fixes\n\n- Improve the error message when a specific package can't be found in the lockfile. [#2358](https://github.com/pdm-project/pdm/issues/2358)\n- prevent wrong project name (including space and illegal characters) [#2360](https://github.com/pdm-project/pdm/issues/2360)\n- Fix a bug that PDM cannot detect namespace packages correctly when creating symlinks. The package's `__init__.py` contains an unusual line. [#2378](https://github.com/pdm-project/pdm/issues/2378)\n- Fix template files created by `pdm init` being read-only when copied from a read-only PDM installation. [#2379](https://github.com/pdm-project/pdm/issues/2379)\n- Don't reset the build backend when asking for import. [#2388](https://github.com/pdm-project/pdm/issues/2388)\n- Never wrap the output of the `export` command. [#2390](https://github.com/pdm-project/pdm/issues/2390)\n- Forbid global project in conda base environment, since it may remove conda-managed packages. [#2409](https://github.com/pdm-project/pdm/issues/2409)\n\n\nRelease v2.10.1 (2023-11-07)\n----------------------------\n\n### Bug Fixes\n\n- Fix a bug preventing ctrl-c from interrupting program execution on 2nd invocation when using \"pdm run\" (Windows only). [#2292](https://github.com/pdm-project/pdm/issues/2292)\n- Fix list index out of range when build error message is empty. [#2337](https://github.com/pdm-project/pdm/issues/2337)\n- Fix find_link sources being exported as `--extra--index-url` [#2342](https://github.com/pdm-project/pdm/issues/2342)\n- Fix an installation failure when install.cache = true. [#2355](https://github.com/pdm-project/pdm/issues/2355)\n- Fix a resolution issue that extra dependencies are not resolved when the bare dependency has more specific version constraint. [#2369](https://github.com/pdm-project/pdm/issues/2369)\n\n### Documentation\n\n- Set up a chatbot powered by LLM on the doc page. [#2365](https://github.com/pdm-project/pdm/issues/2365)\n\n\nRelease v2.10.0 (2023-10-26)\n----------------------------\n\n### Features & Improvements\n\n- Allow binding packages to specific sources with `include_packages` and `exclude_packages` config under `tool.pdm.source` table. [#1645](https://github.com/pdm-project/pdm/issues/1645)\n- Show warnings when a package is rejected by the resolve because of uncovered `requires-python` range. And provide a way to ignore them per-package. [#2304](https://github.com/pdm-project/pdm/issues/2304)\n- Add `-q/--quiet` option to suppress some warnings printed to the console. This option is mutually exclusive with `-v/--verbose`. [#2304](https://github.com/pdm-project/pdm/issues/2304)\n- Introduce a new `--strategy/-S` option for `lock` command, to specify one or more strategy flags for resolving dependencies. `--static-urls` and `--no-cross-platform` are deprecated at the same time. [#2310](https://github.com/pdm-project/pdm/issues/2310)\n- Add lock option to resolve direct dependencies to the minimal versions available. [#2310](https://github.com/pdm-project/pdm/issues/2310)\n- Report the progress of download and unpacking when installing packages. [#2328](https://github.com/pdm-project/pdm/issues/2328)\n\n### Bug Fixes\n\n- Change the venv backend clean function `pdm.cli.commands.venv.backend.Backend._ensure_clean` to empty the `.venv` folder instead of deleting it. [#2282](https://github.com/pdm-project/pdm/issues/2282)\n- Fix a bug that dependency groups from Poetry 1.2+ do not migrate properly to PDM. [#2285](https://github.com/pdm-project/pdm/issues/2285)\n- Fix a bug that build requirements are installed into wrong location when using `--venv` option. [#2314](https://github.com/pdm-project/pdm/issues/2314)\n- Fix a bug that global repository setting results in TypeError . [#2330](https://github.com/pdm-project/pdm/issues/2330)\n- Fix a credentials error when working with two indices on the same host [#2333](https://github.com/pdm-project/pdm/issues/2333)\n\n### Miscellany\n\n- Officially supports python3.12 now. [#2301](https://github.com/pdm-project/pdm/issues/2301)\n\n\nRelease v2.9.3 (2023-09-25)\n---------------------------\n\n### Bug Fixes\n\n- Revert the changes to the behavior of installing self, introduced in #2162.\n  Self package won't be installed when `--no-default` is requested. [#2230](https://github.com/pdm-project/pdm/issues/2230)\n- Reject the candidate if it contains invalid metadata, to avoid a crash in the process of resolution. [#2261](https://github.com/pdm-project/pdm/issues/2261)\n\n### Documentation\n\n- Clarify what `--no-isolated` does. [#2071](https://github.com/pdm-project/pdm/issues/2071)\n\n\nRelease v2.9.2 (2023-09-12)\n---------------------------\n\n### Features & Improvements\n\n- Fix an issue that `--no-lock` option doesn't work as expected. Also support `--no-lock` option for `add`, `remove` and `update` commands. [#2245](https://github.com/pdm-project/pdm/issues/2245)\n\n### Bug Fixes\n\n- Use `findpython` to find pythons with the spec given by the user. [#2225](https://github.com/pdm-project/pdm/issues/2225)\n- Use UTF-8 to read pyvenv.cfg. [#2227](https://github.com/pdm-project/pdm/issues/2227)\n- On Windows, try looking for the `virtualenv` `python.exe` binary under `bin/`\n  as well as `Scripts/` and the `virtualenv`/`conda` root. [#2236](https://github.com/pdm-project/pdm/issues/2236)\n- Write relocatable dependency URLs with `${PROJECT_ROOT}` variable in the lockfile. [#2240](https://github.com/pdm-project/pdm/issues/2240)\n\n\nRelease v2.9.1 (2023-09-03)\n---------------------------\n\n### Features & Improvements\n\n- Support convert setup.cfg without existing setup.py. [#2222](https://github.com/pdm-project/pdm/issues/2222)\n\n### Bug Fixes\n\n- `pdm run` should only find local file if the command starts with `./`. [#2221](https://github.com/pdm-project/pdm/issues/2221)\n\n\nRelease v2.9.0 (2023-08-31)\n---------------------------\n\n### Features & Improvements\n\n- Add an `--overwrite` option to `pdm init` to overwrite existing files(default False). [#2163](https://github.com/pdm-project/pdm/issues/2163)\n- Support passing filter patterns as positional arguments to `pdm list` command.\n  Add `--tree` as an alias and preferred name of `--graph` option. [#2165](https://github.com/pdm-project/pdm/issues/2165)\n- Switch to truststore by default. [#2195](https://github.com/pdm-project/pdm/issues/2195)\n- Consider packages as installed if the venv includes them from the system-site-packages. [#2216](https://github.com/pdm-project/pdm/issues/2216)\n- Allow `pdm run` to run a script with the relative or absolute path. [#2217](https://github.com/pdm-project/pdm/issues/2217)\n\n### Bug Fixes\n\n- Fix a bug that removing dev dependency uninstalls the project as well. [#2150](https://github.com/pdm-project/pdm/issues/2150)\n- Fix a bug that `@ file://` dependencies can not be updated. [#2169](https://github.com/pdm-project/pdm/issues/2169)\n- Fix a bug that dependencies requested out of the range of `requires-python` cause PDM to crash. [#2175](https://github.com/pdm-project/pdm/issues/2175)\n- Fix the compatibility issue with copier 8.0+. [#2177](https://github.com/pdm-project/pdm/issues/2177)\n- Makes `comparable_version(\"1.2.3+local1\") == Version(\"1.2.3\")`. [#2182](https://github.com/pdm-project/pdm/issues/2182)\n- Default behavior for pdm venv activate when shell detection fails. [#2187](https://github.com/pdm-project/pdm/issues/2187)\n- Handle parsing errors when converting from poetry-style metadata. [#2203](https://github.com/pdm-project/pdm/issues/2203)\n- Don't copy .pyc files from the template directory. [#2213](https://github.com/pdm-project/pdm/issues/2213)\n\n### Removals and Deprecations\n\n- Remove the legacy build backend `pdm-pep517`. [#2167](https://github.com/pdm-project/pdm/issues/2167)\n\n\nRelease v2.8.2 (2023-07-31)\n---------------------------\n\n### Features & Improvements\n\n- Allow setting username and password in URL for publish command [#2140](https://github.com/pdm-project/pdm/issues/2140)\n\n### Bug Fixes\n\n- Use UTF-8 encoding when writing `sitecustomize.py`. [#2139](https://github.com/pdm-project/pdm/issues/2139)\n\n\nRelease v2.8.1 (2023-07-26)\n---------------------------\n\n### Features & Improvements\n\n- Add `keyring`, `copier`, `cookiecutter`, `template`, `truststore` dependency groups. [#2109](https://github.com/pdm-project/pdm/issues/2109)\n- Ignore wheels for python versions not in range. [#2113](https://github.com/pdm-project/pdm/issues/2113)\n- Read default value from env var `PDM_PROJECT` for `-p/--project` option. [#2126](https://github.com/pdm-project/pdm/issues/2126)\n\n### Bug Fixes\n\n- Fix the comparison of the candidate keys in the lockfile. [#2120](https://github.com/pdm-project/pdm/issues/2120)\n- Don't update `pyproject.toml` if both `--unconstrained` and `--dry-run` are passed to `pdm update`. [#2125](https://github.com/pdm-project/pdm/issues/2125)\n- Overwrite the `build-system` table when importing from other package manager. [#2126](https://github.com/pdm-project/pdm/issues/2126)\n- Skip sources with empty URL when merging sources. [#2130](https://github.com/pdm-project/pdm/issues/2130)\n- Fix the invalid requirement converted from poetry metadata. [#2133](https://github.com/pdm-project/pdm/issues/2133)\n\n### Dependencies\n\n- Update `unearth` to 0.10.0 [#2113](https://github.com/pdm-project/pdm/issues/2113)\n\n\nRelease v2.8.0 (2023-07-15)\n---------------------------\n\n### Features & Improvements\n\n- Support target python with other architectures. [#2078](https://github.com/pdm-project/pdm/issues/2078)\n- Display the help information when running pdm directly. [#2081](https://github.com/pdm-project/pdm/issues/2081)\n- Allow to change the python providers from the config. Support finding pythons from Rye installation location with the new findpython. [#2099](https://github.com/pdm-project/pdm/issues/2099)\n- Option to save static URLs in the lockfile. By default only filenames are saved. [#2101](https://github.com/pdm-project/pdm/issues/2101)\n\n### Bug Fixes\n\n- Fix a bug that egg-info directories are not removed completely, leading to incomplete distribution. [#2027](https://github.com/pdm-project/pdm/issues/2027)\n- Skip distributions with wrong package meta information and duplicate path. [#2075](https://github.com/pdm-project/pdm/issues/2075)\n- Avoid mistakenly passing command-line arguments while testing. [#2083](https://github.com/pdm-project/pdm/issues/2083)\n- Fix a bug that lockfile groups are overwritten when running locking in a preceding step of `pdm install`. [#2086](https://github.com/pdm-project/pdm/issues/2086)\n- Tolerate and actually ignore the local versions in version specifiers. [#2102](https://github.com/pdm-project/pdm/issues/2102)\n- Fix a bug that shared cache cannot support overlapping namespace packages. [#2105](https://github.com/pdm-project/pdm/issues/2105)\n\n### Documentation\n\n- Add notes about using custom venv path. [#2096](https://github.com/pdm-project/pdm/issues/2096)\n\n\nRelease v2.8.0a2 (2023-06-30)\n-----------------------------\n\n### Bug Fixes\n\n- Fix a bug that dependencies can't be updated when the table is separated by another table. [#2056](https://github.com/pdm-project/pdm/issues/2056)\n- Fix a bug that `*_lock` hooks are always emitted with dry_run=True in `pdm update`. [#2060](https://github.com/pdm-project/pdm/issues/2060)\n- Fix a bug that `pdm install --plugins` can't install self. [#2062](https://github.com/pdm-project/pdm/issues/2062)\n- Fix a cache collision between named requirements and url requirements. [#2064](https://github.com/pdm-project/pdm/issues/2064)\n\n\nRelease v2.8.0a1 (2023-06-27)\n-----------------------------\n\n### Features & Improvements\n\n- Add support for `cookiecutter` and `copier` as project generator. [#2059](https://github.com/pdm-project/pdm/issues/2059)\n\n\nRelease v2.8.0a0 (2023-06-27)\n-----------------------------\n\n### Features & Improvements\n\n- `pdm init` now accepts a template argument to initialize project from a built-in or Git template. [#2053](https://github.com/pdm-project/pdm/issues/2053)\n- Replace the `DeprecationWarning` with `FutureWarning` for better exposure. [#2012](https://github.com/pdm-project/pdm/issues/2012)\n- Serve `install-pdm.py` and its checksum file on the docs site. [#2026](https://github.com/pdm-project/pdm/issues/2026)\n- Add new option `--edit/-e` to `pdm config` to edit the config file in default editor. [#2028](https://github.com/pdm-project/pdm/issues/2028)\n- Add `--project` option to `pdm venv` to support another path as the project root. [#2042](https://github.com/pdm-project/pdm/issues/2042)\n- Add support for using `truststore` as the SSL backend. This only works on Python 3.10 or newer. [#2049](https://github.com/pdm-project/pdm/issues/2049)\n\n### Bug Fixes\n\n- Fix the breaking change by adding the functions back to the old location with deprecation warnings. [#2013](https://github.com/pdm-project/pdm/issues/2013)\n- Fix the duplicate entries in the output of `pdm self list`. [#2018](https://github.com/pdm-project/pdm/issues/2018)\n- Disable hashes caching for local files. [#2019](https://github.com/pdm-project/pdm/issues/2019)\n- Populate the `url` field when converting requirements from a Pipfile-style file requirement. [#2032](https://github.com/pdm-project/pdm/issues/2032)\n- Fix a bug that empty source tables in configuration files causes errors when running pdm commands. [#2034](https://github.com/pdm-project/pdm/issues/2034)\n- Fix a resolution conflict caused by requested yanked version also in other transitive dependencies. [#2038](https://github.com/pdm-project/pdm/issues/2038)\n- Fix a bug that binary executables are corrupted when replacing shebangs. [#2045](https://github.com/pdm-project/pdm/issues/2045)\n- Do not normalize the package name when uploading to PyPI. [#2057](https://github.com/pdm-project/pdm/issues/2057)\n\n\nRelease v2.7.4 (2023-06-13)\n---------------------------\n\nNo significant changes.\n\n\nRelease v2.7.3 (2023-06-13)\n---------------------------\n\n### Bug Fixes\n\n- Fix the warning of extras not found due to extra names not normalized. [#2006](https://github.com/pdm-project/pdm/issues/2006)\n- Pop up a warning when the deprecated `parser` argument is passed to `BaseCommand.__init__()` method. [#2007](https://github.com/pdm-project/pdm/issues/2007)\n- Fix a bug that merging settings with AoTs causing a failure. [#2011](https://github.com/pdm-project/pdm/issues/2011)\n\n\nRelease v2.7.2 (2023-06-12)\n---------------------------\n\n### Features & Improvements\n\n- Add option to expand environment variables when exporting requirements. [#1997](https://github.com/pdm-project/pdm/issues/1997)\n\n### Bug Fixes\n\n- Case-insensitive sorting in `pdm list`. [#1973](https://github.com/pdm-project/pdm/issues/1973)\n- Make a compatible cache reader to read the old cache files. [#1981](https://github.com/pdm-project/pdm/issues/1981)\n- Fix a bug that `pdm init -n` doesn't respect the `--python` option. [#1984](https://github.com/pdm-project/pdm/issues/1984)\n- Do not use the deprecated nested argument groups. [#1988](https://github.com/pdm-project/pdm/issues/1988)\n- Fix an error parsing `setup.py` if it prints something to stdout. [#1995](https://github.com/pdm-project/pdm/issues/1995)\n- Exclude yanked versions when running `install-pdm.py`. [#1996](https://github.com/pdm-project/pdm/issues/1996)\n\n\nRelease v2.7.1 (2023-06-06)\n---------------------------\n\n### Features & Improvements\n\n- Switch HTTP data cache to use a split body setup, where the actual body contents are not written to disk unless changed. Previously, any changed headers would write the whole body to disk again. [#1971](https://github.com/pdm-project/pdm/issues/1971)\n- Show the specific install commands for different installations when checking update. This was removed before. [#1972](https://github.com/pdm-project/pdm/issues/1972)\n\n### Bug Fixes\n\n- PDM ignores env vars `PDM_PYPI_USERNAME` and `PDM_PYPI_PASSWORD` when there are no defaults in config. [#1961](https://github.com/pdm-project/pdm/issues/1961)\n- Guess the project name from VCS url if it is missing when importing from requirements.txt. [#1970](https://github.com/pdm-project/pdm/issues/1970)\n- Correctly read the config from environment variables. [#1977](https://github.com/pdm-project/pdm/issues/1977)\n\n\nRelease v2.7.0 (2023-05-29)\n---------------------------\n\n### Features & Improvements\n\n- When keyring is available, either by importing or by CLI, the credentials of repositories and PyPI indexes will be saved into it. [#1908](https://github.com/pdm-project/pdm/issues/1908)\n- Add support for reading metadata from simple index directly. [#1919](https://github.com/pdm-project/pdm/issues/1919)\n- Add a configuration to specify constant command arguments for every pdm invocation. [#1923](https://github.com/pdm-project/pdm/issues/1923)\n- Add ability to skip SSL verification for publish repositories via `repository.custom.verify_ssl` config option as well as new command line argument of `publish` command. [#1928](https://github.com/pdm-project/pdm/issues/1928)\n- Use lazy import to reduce the startup time of the CLI. [#1929](https://github.com/pdm-project/pdm/issues/1929)\n- Add the local plugin scripts to `PATH` env var. [#1944](https://github.com/pdm-project/pdm/issues/1944)\n\n### Bug Fixes\n\n- Don't use install cache when installing build requirements to avoid race condition. [#1869](https://github.com/pdm-project/pdm/issues/1869)\n- Fix a number of `ResourceWarning`s when running the test suite with warnings enabled. [#1915](https://github.com/pdm-project/pdm/issues/1915)\n- Fix a bug that dev-dependencies group gets updated with the optional dependencies, causing the hash mismatch. [#1916](https://github.com/pdm-project/pdm/issues/1916)\n- Fix format conversion error from Poetry when `tool.poetry.build` doesn't exist. [#1935](https://github.com/pdm-project/pdm/issues/1935)\n- Add timeout when fetching .gitignore from GitHub. [#1937](https://github.com/pdm-project/pdm/issues/1937)\n- Keep the variables in the URL credentials when exporting. [#1939](https://github.com/pdm-project/pdm/issues/1939)\n- Convert to boolean when setting verify_ssl for custom indexes. [#1945](https://github.com/pdm-project/pdm/issues/1945)\n- `pdm import` clobbers `build-system.requires` value in `pyproject.toml`. [#1948](https://github.com/pdm-project/pdm/issues/1948)\n\n### Documentation\n\n- Update publish.md to use run instead of runs to match GitHub Actions steps documentation [#1936](https://github.com/pdm-project/pdm/issues/1936)\n- Update advanced.md to use `pdm sync` instead of `pdm install --no-lock`. [#1947](https://github.com/pdm-project/pdm/issues/1947)\n\n\nRelease v2.6.1 (2023-05-10)\n---------------------------\n\n### Bug Fixes\n\n- Fix the error when publishing using trusted publisher. [#1868](https://github.com/pdm-project/pdm/issues/1868)\n- Fix a bug that `PATH` env var isn't set correctly when running under non-isolation mode. [#1904](https://github.com/pdm-project/pdm/issues/1904)\n\n\nRelease v2.6.0 (2023-05-09)\n---------------------------\n\n### Features & Improvements\n\n- Install project-level plugins from project config, with `tool.pdm.plugins` setting. [#1461](https://github.com/pdm-project/pdm/issues/1461)\n- Added a `--json` flag to both `run` and `info` command allowing to dump scripts and infos as JSON. [#1854](https://github.com/pdm-project/pdm/issues/1854)\n- Consider tasks with a name starting by an underscore (`_`) as internal tasks and hide them from the listing. [#1855](https://github.com/pdm-project/pdm/issues/1855)\n- When running `pdm init -n`(non-interactive mode), a venv will be created by default. Previously, the selected Python will be used under PEP 582 mode. [#1862](https://github.com/pdm-project/pdm/issues/1862)\n- Support [Trusted Publisher](https://docs.pypi.org/trusted-publishers/). [#1868](https://github.com/pdm-project/pdm/issues/1868)\n- Add an ephemeral wheel cache in process for wheels built from non-static revision sources. [#1885](https://github.com/pdm-project/pdm/issues/1885)\n- Allow self-referencing groups in dev-dependencies. [#1890](https://github.com/pdm-project/pdm/issues/1890)\n- Add an option `--no-cross-platform` to `pdm lock` to create a non-cross-platform lockfile. [#1898](https://github.com/pdm-project/pdm/issues/1898)\n\n### Bug Fixes\n\n- Fix brackets in `--venv` option descriptions in zsh completion script. [#1847](https://github.com/pdm-project/pdm/issues/1847)\n- The resolver doesn't take into account of the requirements for both bare `package` and `package[extra]`. [#1851](https://github.com/pdm-project/pdm/issues/1851)\n- Default pypi source does not use configured pypi.password, but \"<hidden>\" instead. [#1856](https://github.com/pdm-project/pdm/issues/1856)\n- Detect Python interpreters under the root of virtual environments. [#1866](https://github.com/pdm-project/pdm/issues/1866)\n- Fix a race condition when the builder is creating a new build directory. [#1869](https://github.com/pdm-project/pdm/issues/1869)\n- Raise `FileNotFoundError` if the requirement path is not found. [#1875](https://github.com/pdm-project/pdm/issues/1875)\n- Fix a bug that the self package isn't uninstallable. [#1901](https://github.com/pdm-project/pdm/issues/1901)\n\n\nRelease v2.5.6 (2023-05-07)\n---------------------------\n\n### Bug Fixes\n\n- Fix a double reading issue due to cachecontrol not compatible with urllib3 2.0. [#1894](https://github.com/pdm-project/pdm/issues/1894)\n\n\nRelease v2.5.5 (2023-05-05)\n---------------------------\n\nNo significant changes.\n\n\nRelease v2.5.4 (2023-05-05)\n---------------------------\n\n### Bug Fixes\n\n- Pin the urllib3 to `<2.0` to avoid incompatibility with `cachecontrol`. [#1886](https://github.com/pdm-project/pdm/issues/1886)\n\n\nRelease v2.5.3 (2023-04-19)\n---------------------------\n\n### Bug Fixes\n\n- Fix the wrong argument validation for update command, where packages given with group option should be allowed. [#1836](https://github.com/pdm-project/pdm/issues/1836)\n\n### Documentation\n\n- Update `markdown-exec` to `1.5.0` for rendering TOC in CLI reference page. [#1836](https://github.com/pdm-project/pdm/issues/1836)\n- Remove advertizing of PEP-582 from the feature highlights. Improve the anchor links for CLI reference. [#1840](https://github.com/pdm-project/pdm/issues/1840)\n\n\nRelease v2.5.2 (2023-04-10)\n---------------------------\n\n### Bug Fixes\n\n- Regression(#1710): Don't crash when trying to update the shebang in a binary script [#1827](https://github.com/pdm-project/pdm/issues/1827)\n- Rename the env var `PDM_USE_VENV` as `PDM_IN_VENV` for `--venv` flag as it mistakenly override another existing env var. [#1829](https://github.com/pdm-project/pdm/issues/1829)\n\n\nRelease v2.5.1 (2023-04-09)\n---------------------------\n\n### Bug Fixes\n\n- Fix a bug that `pdm --pep582` raises an argument error. [#1823](https://github.com/pdm-project/pdm/issues/1823)\n\n\nRelease v2.5.0 (2023-04-09)\n---------------------------\n\n### Features & Improvements\n\n- When `resolution.respect-source-order` is enabled, sources are lazily evaluated. This means that if a match is found on the first source, the remaining sources will not be requested. [#1509](https://github.com/pdm-project/pdm/issues/1509)\n- New option `--venv <venv>` to run a command in the virtual environment with the given name. [#1705](https://github.com/pdm-project/pdm/issues/1705)\n- Allow to prefer binary distributions when locking and installing packages, via `PDM_PREFER_BINARY` environment variable. [#1817](https://github.com/pdm-project/pdm/issues/1817)\n\n### Bug Fixes\n\n- Do not validate selected groups against the locked grouped when running `pdm lock`. [#1796](https://github.com/pdm-project/pdm/issues/1796)\n- Avoid duplicate .pdm-python in .gitignore. [#1800](https://github.com/pdm-project/pdm/issues/1800)\n- Fix a backwards compatibility issue by adding back the `environment.is_global` property. [#1814](https://github.com/pdm-project/pdm/issues/1814)\n- Fix a resolution conflict when a relative path requirement resolves to the same path as another file requirement with absolute path. [#1822](https://github.com/pdm-project/pdm/issues/1822)\n- Fix an error when running `pdm init -p <dir>` if the target directory is not created yet. [#1822](https://github.com/pdm-project/pdm/issues/1822)\n\n\nRelease v2.5.0b0 (2023-03-29)\n-----------------------------\n\n### Breaking Changes\n\n- Switch the default build backend to `pdm-backend`. [#1684](https://github.com/pdm-project/pdm/issues/1684)\n- Only lock selected groups into the lockfile. Modify other commands to honor the groups included in the lockfile. [#1704](https://github.com/pdm-project/pdm/issues/1704)\n- Move the project python path to its own file, and rename the project config file as `pdm.toml` which can be committed to the VCS. [#1742](https://github.com/pdm-project/pdm/issues/1742)\n- Refactor the environment package. `Environment` is renamed to `PythonLocalEnvironment` and `GlobalEnvironment` is renamed to `PythonEnvironment`. Move `pdm.models.environment` module to `pdm.environments` package. [#1791](https://github.com/pdm-project/pdm/issues/1791)\n\n### Features & Improvements\n\n- Add option to fail on the first install error. [#1614](https://github.com/pdm-project/pdm/issues/1614)\n- Upgrade `unearth` to 0.8 to allow calling keyring from CLI. [#1653](https://github.com/pdm-project/pdm/issues/1653)\n- Merge the index parameters from different configuration files. [#1667](https://github.com/pdm-project/pdm/issues/1667)\n- Add new options to `venv` command to show the path or the python interpreter for a managed venv. [#1680](https://github.com/pdm-project/pdm/issues/1680)\n- Write the groups of resolved dependencies to the metadata table in lockfile. [#1692](https://github.com/pdm-project/pdm/issues/1692)\n- Introduce `--lib` option to `init` command to create a library project without prompting. [#1708](https://github.com/pdm-project/pdm/issues/1708)\n- New command: `pdm fix` to migrate to the new PDM features. Add a hint when invoking PDM commands. [#1743](https://github.com/pdm-project/pdm/issues/1743)\n- Include `.pdm-python` in project root `.gitignore` when running `pdm init`. [#1749](https://github.com/pdm-project/pdm/issues/1749)\n- Allow to ignore the activated venv with `PDM_IGNORE_ACTIVE_VENV` env var. [#1782](https://github.com/pdm-project/pdm/issues/1782)\n- Add a signal `pre_invoke` to emit before any command is invoked. [#1792](https://github.com/pdm-project/pdm/issues/1792)\n\n### Bug Fixes\n\n- Fix a bug that install warning prints to terminal under non-verbose mode. [#1635](https://github.com/pdm-project/pdm/issues/1635)\n- Fix the random failure of `pdm export` due to non-deterministic order of group iteration. [#1786](https://github.com/pdm-project/pdm/issues/1786)\n- Show the actual version when running `pdm show --version` [#1788](https://github.com/pdm-project/pdm/issues/1788)\n\n### Documentation\n\n- Restructure the documentation. [#1687](https://github.com/pdm-project/pdm/issues/1687)\n\n### Dependencies\n\n- Update `installer` to `0.7.0` and emit a warning if the RECORD validation fails. [#1784](https://github.com/pdm-project/pdm/issues/1784)\n\n\nRelease v2.4.9 (2023-03-16)\n---------------------------\n\n### Bug Fixes\n\n- Fix a bug of synchronization of not considering the revision of VCS requirement in comparison. [#1762](https://github.com/pdm-project/pdm/issues/1762)\n- Improve the error message when parsing an invalid requirement string. [#1765](https://github.com/pdm-project/pdm/issues/1765)\n- Fix a bug that `pdm export` output doesn't include the extras of the dependencies. [#1767](https://github.com/pdm-project/pdm/issues/1767)\n\n\nRelease v2.4.8 (2023-03-09)\n---------------------------\n\n### Bug Fixes\n\n- Fix the resolution order to prefer the packages causing the conflict.\n  This can make the resolution reach a solution faster. [#1752](https://github.com/pdm-project/pdm/issues/1752)\n- Fix a bug that embedded credentials in URL are not respected for the default source. [#1757](https://github.com/pdm-project/pdm/issues/1757)\n\n\nRelease v2.4.7 (2023-03-02)\n---------------------------\n\n### Bug Fixes\n\n- Abort if lockfile isn't generated when executing `pdm export`. [#1730](https://github.com/pdm-project/pdm/issues/1730)\n- Ignore `venv.prompt` configuration when using `conda` as the backend. [#1734](https://github.com/pdm-project/pdm/issues/1734)\n- Fix a bug of finding local packages in the parent folder when it exists in the current folder. [#1736](https://github.com/pdm-project/pdm/issues/1736)\n- Ensure UTF-8 encoding when generating README.md. [#1739](https://github.com/pdm-project/pdm/issues/1739)\n- Fix a bug of show command not showing metadata of the current project. [#1740](https://github.com/pdm-project/pdm/issues/1740)\n- Replace `.` with `-` when normalizing package name. [#1745](https://github.com/pdm-project/pdm/issues/1745)\n\n### Documentation\n\n- Support using `pdm venv activate` without specifying `env_name` to activate in project venv created by conda [#1735](https://github.com/pdm-project/pdm/issues/1735)\n\n\nRelease v2.4.6 (2023-02-20)\n---------------------------\n\n### Bug Fixes\n\n- Fix a resolution failure when the project has cascading relative path dependencies. [#1702](https://github.com/pdm-project/pdm/issues/1702)\n- Don't crash when trying to update the shebang in a binary script. [#1709](https://github.com/pdm-project/pdm/issues/1709)\n- Handle the legacy specifiers that is unable to parse with packaging>22.0. [#1719](https://github.com/pdm-project/pdm/issues/1719)\n- Fix the setup.py parser to ignore the expressions unable to parse as a string. This is safe for initializing a requirement. [#1720](https://github.com/pdm-project/pdm/issues/1720)\n- Fix a bug converting from flit metadata when the source file can't be found. [#1726](https://github.com/pdm-project/pdm/issues/1726)\n\n### Documentation\n\n- Add config example for Emacs using eglot + pyright [#1721](https://github.com/pdm-project/pdm/issues/1721)\n\n### Miscellany\n\n- Use `ruff` as the linter. [#1715](https://github.com/pdm-project/pdm/issues/1715)\n- Document installation via `asdf`. [#1725](https://github.com/pdm-project/pdm/issues/1725)\n\n\nRelease v2.4.5 (2023-02-10)\n---------------------------\n\n### Bug Fixes\n\n- Fix a bug that built wheels are prioritized over source distributions with higher version number. [#1698](https://github.com/pdm-project/pdm/issues/1698)\n\n\nRelease v2.4.4 (2023-02-10)\n---------------------------\n\n### Features & Improvements\n\n- Add more intuitive error message when the `requires-python` doesn't work for all dependencies. [#1690](https://github.com/pdm-project/pdm/issues/1690)\n\n### Bug Fixes\n\n- Prefer built distributions when finding packages for metadata extraction. [#1535](https://github.com/pdm-project/pdm/issues/1535)\n\n\nRelease v2.4.3 (2023-02-06)\n---------------------------\n\n### Features & Improvements\n\n- Allow creating venv in project forcibly if it already exists. [#1666](https://github.com/pdm-project/pdm/issues/1666)\n- Always ignore remembered selection in pdm init. [#1672](https://github.com/pdm-project/pdm/issues/1672)\n\n### Bug Fixes\n\n- Fix the fallback build backend to `pdm-pep517` instead of `setuptools`. [#1658](https://github.com/pdm-project/pdm/issues/1658)\n- Eliminate the deprecation warnings from `importlib.resources`. [#1660](https://github.com/pdm-project/pdm/issues/1660)\n- Don't crash when failed to get the latest version of PDM for checking update. [#1663](https://github.com/pdm-project/pdm/issues/1663)\n- Fix the priorities of importable formats to make sure the correct format is used. [#1669](https://github.com/pdm-project/pdm/issues/1669)\n- Import editable requirements into dev dependencies. [#1674](https://github.com/pdm-project/pdm/issues/1674)\n\n\nRelease v2.4.2 (2023-01-31)\n---------------------------\n\n### Bug Fixes\n\n- Skip some tests on packaging < 22. [#1649](https://github.com/pdm-project/pdm/issues/1649)\n- Fix a bug that sources from the project config are not loaded. [#1651](https://github.com/pdm-project/pdm/issues/1651)\n- Set VIRTUAL_ENV in `pdm run`. [#1652](https://github.com/pdm-project/pdm/issues/1652)\n\n\nRelease v2.4.1 (2023-01-28)\n---------------------------\n\n### Features & Improvements\n\n- Add proper display for the extra pypi sources in `pdm config`. [#1622](https://github.com/pdm-project/pdm/issues/1622)\n- Support running python scripts without prefixing with `python`. [#1626](https://github.com/pdm-project/pdm/issues/1626)\n\n### Bug Fixes\n\n- Ignore the python requirement for overridden packages. [#1575](https://github.com/pdm-project/pdm/issues/1575)\n- Fix the wildcards in requirement specifiers to make it pass the new parser of `packaging>=22`. [#1619](https://github.com/pdm-project/pdm/issues/1619)\n- Add the missing `subdirectory` attribute to the lockfile entry. [#1630](https://github.com/pdm-project/pdm/issues/1630)\n- Fix a bug that VCS locks don't update when the rev part changes. [#1640](https://github.com/pdm-project/pdm/issues/1640)\n- Redirect the spinner output to stderr. [#1646](https://github.com/pdm-project/pdm/issues/1646)\n- Ensure the destination directory exists before building the packages. [#1647](https://github.com/pdm-project/pdm/issues/1647)\n\n\nRelease v2.4.0 (2023-01-12)\n---------------------------\n\n### Features & Improvements\n\n- Support multiple PyPI indexes in the configuration. They will be tried after the sources in `pyproject.toml`. [#1310](https://github.com/pdm-project/pdm/issues/1310)\n- Accept yanked versions when the requirement version is pinned. [#1575](https://github.com/pdm-project/pdm/issues/1575)\n- Expose PDM fixtures as a `pytest` plugin `pdm.pytest` for plugin developers. [#1594](https://github.com/pdm-project/pdm/issues/1594)\n- Show message in the status when fetching package hashes.\n  Fetch hashes from the JSON API response as well. [#1609](https://github.com/pdm-project/pdm/issues/1609)\n- Mark `pdm.lock` with an `@generated` comment. [#1611](https://github.com/pdm-project/pdm/issues/1611)\n\n### Bug Fixes\n\n- Exclude site-packages for symlinks of the python interpreter as well. [#1598](https://github.com/pdm-project/pdm/issues/1598)\n- Fix a bug that error output can't be decoded correctly on Windows. [#1602](https://github.com/pdm-project/pdm/issues/1602)\n\n\nRelease v2.3.4 (2022-12-27)\n---------------------------\n\n### Features & Improvements\n\n- Detect PDM inside a zipapp and disable some functions. [#1578](https://github.com/pdm-project/pdm/issues/1578)\n\n### Bug Fixes\n\n- Don't write `sitecustomize` to the home directory if it exists in the filesystem(not packed in a zipapp). [#1572](https://github.com/pdm-project/pdm/issues/1572)\n- Fix a bug that a directory is incorrectly marked as to be deleted when it contains symlinks. [#1580](https://github.com/pdm-project/pdm/issues/1580)\n\n\nRelease v2.3.3 (2022-12-15)\n---------------------------\n\n### Bug Fixes\n\n- Allow relative paths in `build-system.requires`, since `build` and `hatch` both support it. Be aware it is not allowed in the standard. [#1560](https://github.com/pdm-project/pdm/issues/1560)\n- Strip the local part when building a specifier for comparison with the package version. This is not permitted by PEP 508 as implemented by `packaging 22.0`. [#1562](https://github.com/pdm-project/pdm/issues/1562)\n- Update the version for check_update after self update [#1563](https://github.com/pdm-project/pdm/issues/1563)\n- Replace the `__file__` usages with `importlib.resources`, to make PDM usable in a zipapp. [#1567](https://github.com/pdm-project/pdm/issues/1567)\n- Fix the matching problem of packages in the lockfile. [#1569](https://github.com/pdm-project/pdm/issues/1569)\n\n### Dependencies\n\n- Exclude `package==22.0` from the dependencies to avoid some breakages to the end users. [#1568](https://github.com/pdm-project/pdm/issues/1568)\n\n\nRelease v2.3.2 (2022-12-08)\n---------------------------\n\n### Bug Fixes\n\n- Fix an installation failure when the RECORD file contains commas in the file path. [#1010](https://github.com/pdm-project/pdm/issues/1010)\n- Fallback to `pdm.pep517` as the metadata transformer for unknown custom build backends. [#1546](https://github.com/pdm-project/pdm/issues/1546)\n- Fix a bug that Ctrl + C kills the python interactive session instead of clearing the current line. [#1547](https://github.com/pdm-project/pdm/issues/1547)\n- Fix a bug with egg segment for local dependency [#1552](https://github.com/pdm-project/pdm/issues/1552)\n\n### Dependencies\n\n- Update `installer` to `0.6.0`. [#1550](https://github.com/pdm-project/pdm/issues/1550)\n- Update minimum version of `unearth` to `0.6.3` and test against `packaging==22.0`. [#1555](https://github.com/pdm-project/pdm/issues/1555)\n\n\nRelease v2.3.1 (2022-12-05)\n---------------------------\n\n### Bug Fixes\n\n- Fix a resolution loop issue when the current project depends on itself and it uses the dynamic version from SCM. [#1541](https://github.com/pdm-project/pdm/issues/1541)\n- Don't give duplicate results when specifying a relative path for `pdm use`. [#1542](https://github.com/pdm-project/pdm/issues/1542)\n\n\nRelease v2.3.0 (2022-12-02)\n---------------------------\n\n### Features & Improvements\n\n- Beautify the error message of build errors. Default to showing the last 10 lines of the build output. [#1491](https://github.com/pdm-project/pdm/issues/1491)\n- Rename the `tool.pdm.overrides` table to `tool.pdm.resolution.overrides`. The old name is deprecated at the same time. [#1503](https://github.com/pdm-project/pdm/issues/1503)\n- Add backend selection and `--backend` option to `pdm init` command, users can choose a favorite backend from `setuptools`, `flit`, `hatchling` and `pdm-pep517`(default), since they all support PEP 621 standards. [#1504](https://github.com/pdm-project/pdm/issues/1504)\n- Allows specifying the insertion position of user provided arguments in scripts with the `{args[:default]}` placeholder. [#1507](https://github.com/pdm-project/pdm/issues/1507)\n\n### Bug Fixes\n\n- The local package is now treated specially during installation and locking. This means it will no longer be included in the lockfile, and should never be installed twice even when using nested extras. This will ensure the lockdown stays relevant when the version changes. [#1481](https://github.com/pdm-project/pdm/issues/1481)\n- Fix the version diff algorithm of installed packages to consider local versions as compatible. [#1497](https://github.com/pdm-project/pdm/issues/1497)\n- Fix the confusing message when detecting a Python interpreter under `python.use_venv=False` [#1508](https://github.com/pdm-project/pdm/issues/1508)\n- Fix the test failure with the latest `findpython` installed. [#1516](https://github.com/pdm-project/pdm/issues/1516)\n- Fix the module missing error of pywin32 in a virtualenv with `install.cache` set to `true` and caching method is `pth`. [#863](https://github.com/pdm-project/pdm/issues/863)\n\n### Dependencies\n\n- Drop the dependency `pdm-pep517`. [#1504](https://github.com/pdm-project/pdm/issues/1504)\n- Replace `pep517` with `pyproject-hooks` because of the rename. [#1528](https://github.com/pdm-project/pdm/issues/1528)\n\n### Removals and Deprecations\n\n- Remove the support for exporting the project file to a `setup.py` format, users are encouraged to migrate to the PEP 621 metadata. [#1504](https://github.com/pdm-project/pdm/issues/1504)\n\n\nRelease v2.2.1 (2022-11-03)\n---------------------------\n\n### Features & Improvements\n\n- Make `sitecustomize.py` respect the `PDM_PROJECT_MAX_DEPTH` environment variable [#1471](https://github.com/pdm-project/pdm/issues/1471)\n\n### Bug Fixes\n\n- Fix the comparison of `python_version` in the environment marker. When the version contains only one digit, the result was incorrect. [#1484](https://github.com/pdm-project/pdm/issues/1484)\n\n\nRelease v2.2.0 (2022-10-31)\n---------------------------\n\n### Features & Improvements\n\n- Add `venv.prompt` configuration to allow customizing prompt when a virtualenv is activated [#1332](https://github.com/pdm-project/pdm/issues/1332)\n- Allow the use of custom CA certificates per publish repository using `ca_certs` or from the command line via `pdm publish --ca-certs <path> ...`. [#1392](https://github.com/pdm-project/pdm/issues/1392)\n- Rename the `plugin` command to `self`, and it can not only manage plugins but also all dependencies. Add a subcommand `self update` to update PDM itself. [#1406](https://github.com/pdm-project/pdm/issues/1406)\n- Allow `pdm init` to receive a Python path or version via `--python` option. [#1412](https://github.com/pdm-project/pdm/issues/1412)\n- Add a default value for `requires-python` when importing from other formats. [#1426](https://github.com/pdm-project/pdm/issues/1426)\n- Use `pdm` instead of `pip` to resolve and install build requirements. So that PDM configurations can control the process. [#1429](https://github.com/pdm-project/pdm/issues/1429)\n- Customizable color theme via `pdm config` command. [#1450](https://github.com/pdm-project/pdm/issues/1450)\n- A new `pdm lock --check` flag to validate whether the lock is up to date. [#1459](https://github.com/pdm-project/pdm/issues/1459)\n- Add both option and config item to ship `pip` when creating a new venv. [#1463](https://github.com/pdm-project/pdm/issues/1463)\n- Issue warning and skip the requirement if it has the same name as the current project. [#1466](https://github.com/pdm-project/pdm/issues/1466)\n- Enhance the `pdm list` command with new formats: `--csv,--markdown` and add options `--fields,--sort` to control the output contents. Users can also include `licenses` in the `--fields` option to display the package licenses. [#1469](https://github.com/pdm-project/pdm/issues/1469)\n- A new pre-commit hook to run `pdm lock --check` in pre-commit. [#1471](https://github.com/pdm-project/pdm/issues/1471)\n\n### Bug Fixes\n\n- Fix the issue that relative paths don't work well with `--project` argument. [#1220](https://github.com/pdm-project/pdm/issues/1220)\n- It is now possible to refer to a package from outside the project with relative paths in dependencies. [#1381](https://github.com/pdm-project/pdm/issues/1381)\n- Ensure `pypi.[ca,client]_cert[s]` config items are passed to distribution builder install steps to allow for custom PyPI index sources with self signed certificates. [#1396](https://github.com/pdm-project/pdm/issues/1396)\n- Fix a crash issue when depending on editable packages with extras. [#1401](https://github.com/pdm-project/pdm/issues/1401)\n- Do not save the python path when using non-interactive mode in `pdm init`. [#1410](https://github.com/pdm-project/pdm/issues/1410)\n- Fix the matching of `python*` command in `pdm run`. [#1414](https://github.com/pdm-project/pdm/issues/1414)\n- Show the Python path, instead of the real executable, in the Python selection menu. [#1418](https://github.com/pdm-project/pdm/issues/1418)\n- Fix the HTTP client of package publishment to prompt for password and read PDM configurations correctly. [#1430](https://github.com/pdm-project/pdm/issues/1430)\n- Ignore the unknown fields when constructing a requirement object. [#1445](https://github.com/pdm-project/pdm/issues/1445)\n- Fix a bug of unrelated candidates being fetched if the requirement is matching wildcard versions(e.g. `==1.*`). [#1465](https://github.com/pdm-project/pdm/issues/1465)\n- Use `importlib-metadata` from PyPI for Python < 3.10. [#1467](https://github.com/pdm-project/pdm/issues/1467)\n\n### Documentation\n\n- Clarify the difference between a library and an application. Update the guide of multi-stage docker build. [#1371](https://github.com/pdm-project/pdm/issues/1371)\n\n### Removals and Deprecations\n\n- Remove all top-level imports, users should import from the submodules instead. [#1404](https://github.com/pdm-project/pdm/issues/1404)\n- Remove the usages of old config names deprecated since 2.0. [#1422](https://github.com/pdm-project/pdm/issues/1422)\n- Remove the deprecated color functions, use [rich's console markup](https://rich.readthedocs.io/en/latest/markup.html) instead. [#1452](https://github.com/pdm-project/pdm/issues/1452)\n\n\nRelease v2.1.5 (2022-10-05)\n---------------------------\n\n### Bug Fixes\n\n- Ensure `pypi.[ca,client]_cert[s]` config items are passed to distribution builder install steps to allow for custom PyPI index sources with self signed certificates. [#1396](https://github.com/pdm-project/pdm/issues/1396)\n- Fix a crash issue when depending on editable packages with extras. [#1401](https://github.com/pdm-project/pdm/issues/1401)\n- Do not save the python path when using non-interactive mode in `pdm init`. [#1410](https://github.com/pdm-project/pdm/issues/1410)\n- Restrict importlib-metadata (<5.0.0) for Python <3.8 [#1411](https://github.com/pdm-project/pdm/issues/1411)\n\n\nRelease v2.1.4 (2022-09-17)\n---------------------------\n\n### Bug Fixes\n\n- Fix a lock failure when depending on self with URL requirements. [#1347](https://github.com/pdm-project/pdm/issues/1347)\n- Ensure list to concatenate args for composite scripts. [#1359](https://github.com/pdm-project/pdm/issues/1359)\n- Fix an error in `pdm lock --refresh` if some packages has URLs. [#1361](https://github.com/pdm-project/pdm/issues/1361)\n- Fix unnecessary package downloads and VCS clones for certain commands. [#1370](https://github.com/pdm-project/pdm/issues/1370)\n- Fix a conversion error when converting a list of conditional dependencies from a Poetry format. [#1383](https://github.com/pdm-project/pdm/issues/1383)\n\n### Documentation\n\n- Adds a section to the docs on how to correctly work with PDM and version control systems. [#1364](https://github.com/pdm-project/pdm/issues/1364)\n\n\nRelease v2.1.3 (2022-08-30)\n---------------------------\n\n### Features & Improvements\n\n- When adding a package to (or removing from) a group, enhance the formatting of the group name in the printed message. [#1329](https://github.com/pdm-project/pdm/issues/1329)\n\n### Bug Fixes\n\n- Fix a bug of missing hashes for packages with `file://` links the first time they are added. [#1325](https://github.com/pdm-project/pdm/issues/1325)\n- Ignore invalid values of `data-requires-python` when parsing package links. [#1334](https://github.com/pdm-project/pdm/issues/1334)\n- Leave an incomplete project metadata if PDM fails to parse the project files, but emit a warning. [#1337](https://github.com/pdm-project/pdm/issues/1337)\n- Fix the bug that `editables` package isn't installed for self package. [#1344](https://github.com/pdm-project/pdm/issues/1344)\n- Fix a decoding error for non-ASCII characters in package description when publishing it. [#1345](https://github.com/pdm-project/pdm/issues/1345)\n\n### Documentation\n\n- Clarify documentation explaining `setup-script`, `run-setuptools`, and `is-purelib`. [#1327](https://github.com/pdm-project/pdm/issues/1327)\n\n\nRelease v2.1.2 (2022-08-15)\n---------------------------\n\n### Bug Fixes\n\n- Fix a bug that dependencies from different versions of the same package override each other. [#1307](https://github.com/pdm-project/pdm/issues/1307)\n- Forward SIGTERM to child processes in `pdm run`. [#1312](https://github.com/pdm-project/pdm/issues/1312)\n- Fix errors when running on FIPS 140-2 enabled systems using Python 3.9 and newer. [#1313](https://github.com/pdm-project/pdm/issues/1313)\n- Fix the build failure when the subprocess outputs with non-UTF8 characters. [#1319](https://github.com/pdm-project/pdm/issues/1319)\n- Delay the trigger of `post_lock` for `add` and `update` operations, to ensure the `pyproject.toml` is updated before the hook is run. [#1320](https://github.com/pdm-project/pdm/issues/1320)\n\n\nRelease v2.1.1 (2022-08-05)\n---------------------------\n\n### Features & Improvements\n\n- Add a env_file.override option that allows the user to specify that\n  the env_file should override any existing environment variables. This\n  is not the default as the environment the code runs it should take\n  precedence. [#1299](https://github.com/pdm-project/pdm/issues/1299)\n\n### Bug Fixes\n\n- Fix a bug that unnamed requirements can't override the old ones in either `add` or `update` command. [#1287](https://github.com/pdm-project/pdm/issues/1287)\n- Support mutual TLS to private repositories via pypi.client_cert and pypi.client_key config options. [#1290](https://github.com/pdm-project/pdm/issues/1290)\n- Set a minimum version for the `packaging` dependency to ensure that `packaging.utils.parse_wheel_filename` is available. [#1293](https://github.com/pdm-project/pdm/issues/1293)\n- Fix a bug that checking for PDM update creates a venv. [#1301](https://github.com/pdm-project/pdm/issues/1301)\n- Prefer compatible packages when fetching metadata. [#1302](https://github.com/pdm-project/pdm/issues/1302)\n\n\nRelease v2.1.0 (2022-07-29)\n---------------------------\n\n### Features & Improvements\n\n- Allow the use of custom CA certificates using the `pypi.ca_certs` config entry. [#1240](https://github.com/pdm-project/pdm/issues/1240)\n- Add `pdm export` to available pre-commit hooks. [#1279](https://github.com/pdm-project/pdm/issues/1279)\n\n### Bug Fixes\n\n- Skip incompatible requirements when installing build dependencies. [#1264](https://github.com/pdm-project/pdm/issues/1264)\n- Fix a crash when pdm tries to publish a package with non-ASCII characters in the metadata. [#1270](https://github.com/pdm-project/pdm/issues/1270)\n- Try to read the lock file even if the lock version is incompatible. [#1273](https://github.com/pdm-project/pdm/issues/1273)\n- For packages that are only available as source distribution, the `summary` field in `pdm.lock` contains the `description` from the package's `pyproject.toml`. [#1274](https://github.com/pdm-project/pdm/issues/1274)\n- Do not crash when calling `pdm show` for a package that is only available as source distribution. [#1276](https://github.com/pdm-project/pdm/issues/1276)\n- Fix a bug that completion scripts are interpreted as rich markups. [#1283](https://github.com/pdm-project/pdm/issues/1283)\n\n### Dependencies\n\n- Remove the dependency of `pip`. [#1268](https://github.com/pdm-project/pdm/issues/1268)\n\n### Removals and Deprecations\n\n- Deprecate the top-level imports from `pdm` module, it will be removed in the future. [#1282](https://github.com/pdm-project/pdm/issues/1282)\n\n\nRelease v2.0.3 (2022-07-22)\n---------------------------\n\n### Bug Fixes\n\n- Support Conda environments when detecting the project environment. [#1253](https://github.com/pdm-project/pdm/issues/1253)\n- Fix the interpreter resolution to first try `python` executable in the `PATH`. [#1255](https://github.com/pdm-project/pdm/issues/1255)\n- Stabilize sorting of URLs in `metadata.files` in `pdm.lock`. [#1256](https://github.com/pdm-project/pdm/issues/1256)\n- Don't expand credentials in the file URLs in the `[metadata.files]` table of the lock file. [#1259](https://github.com/pdm-project/pdm/issues/1259)\n\n\nRelease v2.0.2 (2022-07-20)\n---------------------------\n\n### Features & Improvements\n\n- `env_file` variables no longer override existing environment variables. [#1235](https://github.com/pdm-project/pdm/issues/1235)\n- Support referencing other optional groups in optional-dependencies with `<this_package_name>[group1, group2]` [#1241](https://github.com/pdm-project/pdm/issues/1241)\n\n### Bug Fixes\n\n- Respect `requires-python` when creating the default venv. [#1237](https://github.com/pdm-project/pdm/issues/1237)\n\n\nRelease v2.0.1 (2022-07-17)\n---------------------------\n\n### Bug Fixes\n\n- Write lockfile before calling 'post_lock' hook [#1224](https://github.com/pdm-project/pdm/issues/1224)\n- Suppress errors when cache dir isn't accessible. [#1226](https://github.com/pdm-project/pdm/issues/1226)\n- Don't save python path for venv commands. [#1230](https://github.com/pdm-project/pdm/issues/1230)\n\n\nRelease v2.0.0 (2022-07-15)\n---------------------------\n\n### Bug Fixes\n\n- Fix a bug that the running env overrides the PEP 582 `PYTHONPATH`. [#1211](https://github.com/pdm-project/pdm/issues/1211)\n- Add [`pwsh`](https://github.com/PowerShell/PowerShell) as an alias of `powershell` for shell completion. [#1216](https://github.com/pdm-project/pdm/issues/1216)\n- Fixed a bug with `zsh` completion regarding `--pep582` flag. [#1218](https://github.com/pdm-project/pdm/issues/1218)\n- Fix a bug of requirement checking under non-isolated mode. [#1219](https://github.com/pdm-project/pdm/issues/1219)\n- Fix a bug when removing packages, TOML document might become invalid. [#1221](https://github.com/pdm-project/pdm/issues/1221)\n\n\nRelease v2.0.0b2 (2022-07-08)\n-----------------------------\n\n### Breaking Changes\n\n- Store file URLs instead of filenames in the lock file, bump lock version to `4.0`. [#1203](https://github.com/pdm-project/pdm/issues/1203)\n\n### Features & Improvements\n\n- Read site-wide configuration, which serves as the lowest-priority layer.\n  This layer will be read-only in the CLI. [#1200](https://github.com/pdm-project/pdm/issues/1200)\n- Get package links from the urls stored in the lock file. [#1204](https://github.com/pdm-project/pdm/issues/1204)\n\n### Bug Fixes\n\n- Fix a bug that the host pip(installed with pdm) may not be compatible with the project python. [#1196](https://github.com/pdm-project/pdm/issues/1196)\n- Update `unearth` to fix a bug that install links with weak hashes are skipped. This often happens on self-hosted PyPI servers. [#1202](https://github.com/pdm-project/pdm/issues/1202)\n\n\nRelease v2.0.0b1 (2022-07-02)\n-----------------------------\n\n### Features & Improvements\n\n- Integrate `pdm venv` commands into the main program. Make PEP 582 an opt-in feature. [#1162](https://github.com/pdm-project/pdm/issues/1162)\n- Add config `global_project.fallback_verbose` defaulting to `True`. When set to `False` disables message `Project is not found, fallback to the global project` [#1188](https://github.com/pdm-project/pdm/issues/1188)\n- Add `--only-keep` option to `pdm sync` to keep only selected packages. Originally requested at #398. [#1191](https://github.com/pdm-project/pdm/issues/1191)\n\n### Bug Fixes\n\n- Fix a bug that requirement extras and underlying are resolved to the different version [#1173](https://github.com/pdm-project/pdm/issues/1173)\n- Update `unearth` to `0.4.1` to skip the wheels with invalid version parts. [#1178](https://github.com/pdm-project/pdm/issues/1178)\n- Fix reading `PDM_RESOLVE_MAX_ROUNDS` environment variable (was spelled `…ROUDNS` before). [#1180](https://github.com/pdm-project/pdm/issues/1180)\n- Deduplicate the list of found Python versions. [#1182](https://github.com/pdm-project/pdm/issues/1182)\n- Use the normal stream handler for logging, to fix some display issues under non-tty environments. [#1184](https://github.com/pdm-project/pdm/issues/1184)\n\n### Removals and Deprecations\n\n- Remove the useless `--no-clean` option from `pdm sync` command. [#1191](https://github.com/pdm-project/pdm/issues/1191)\n\n\nRelease v2.0.0a1 (2022-06-29)\n-----------------------------\n\n### Breaking Changes\n\n- Editable dependencies in the `[project]` table is not allowed, according to PEP 621. They are however still allowed in the `[tool.pdm.dev-dependencies]` table. PDM will emit a warning when it finds editable dependencies in the `[project]` table, or will abort when you try to add them into the `[project]` table via CLI. [#1083](https://github.com/pdm-project/pdm/issues/1083)\n- Now the paths to the global configurations and global project are calculated according to platform standards. [#1161](https://github.com/pdm-project/pdm/issues/1161)\n\n### Features & Improvements\n\n- Add support for importing from a `setup.py` project. [#1062](https://github.com/pdm-project/pdm/issues/1062)\n- Switch the UI backend to `rich`. [#1091](https://github.com/pdm-project/pdm/issues/1091)\n- Improved the terminal UI and logging. Disable live progress under verbose mode. The logger levels can be controlled by the `-v` option. [#1096](https://github.com/pdm-project/pdm/issues/1096)\n- Use `unearth` to replace `pip`'s `PackageFinder` and related data models. PDM no longer relies on `pip` internals, which are unstable across updates. [#1096](https://github.com/pdm-project/pdm/issues/1096)\n- Lazily load the candidates returned by `find_matches()` to speed up the resolution. [#1098](https://github.com/pdm-project/pdm/issues/1098)\n- Add a new command `publish` to PDM since it is required for so many people and it will make the workflow easier. [#1107](https://github.com/pdm-project/pdm/issues/1107)\n- Add a `composite` script kind allowing to run multiple defined scripts in a single command as well as reusing scripts but overriding `env` or `env_file`. [#1117](https://github.com/pdm-project/pdm/issues/1117)\n- Add a new execution option `--skip` to opt-out some scripts and hooks from any execution (both scripts and PDM commands). [#1127](https://github.com/pdm-project/pdm/issues/1127)\n- Add the `pre/post_publish`, `pre/post_run` and `pre/post_script` hooks as well as an extensive lifecycle and hooks documentation. [#1147](https://github.com/pdm-project/pdm/issues/1147)\n- Shorter scripts listing, especially for multilines and composite scripts. [#1151](https://github.com/pdm-project/pdm/issues/1151)\n- Build configurations have been moved to `[tool.pdm.build]`, according to `pdm-pep517 1.0.0`. At the same time, warnings will be shown against old usages. [#1153](https://github.com/pdm-project/pdm/issues/1153)\n- Improve the lock speed by parallelizing the hash fetching. [#1154](https://github.com/pdm-project/pdm/issues/1154)\n- Retrieve the candidate metadata by parsing the `pyproject.toml` rather than building it. [#1156](https://github.com/pdm-project/pdm/issues/1156)\n- Update the format converters to support the new `[tool.pdm.build]` table. [#1157](https://github.com/pdm-project/pdm/issues/1157)\n- Scripts are now available as root command if they don't conflict with any builtin or plugin-contributed command. [#1159](https://github.com/pdm-project/pdm/issues/1159)\n- Add a `post_use` hook triggered after successfully switching Python version. [#1163](https://github.com/pdm-project/pdm/issues/1163)\n- Add project configuration `respect-source-order` under `[tool.pdm.resolution]` to respect the source order in the `pyproject.toml` file. Packages will be returned by source earlier in the order or later ones if not found. [#593](https://github.com/pdm-project/pdm/issues/593)\n\n### Bug Fixes\n\n- Fix a bug that candidates with local part in the version can't be found and installed correctly. [#1093](https://github.com/pdm-project/pdm/issues/1093)\n\n### Dependencies\n\n- Prefer `tomllib` on Python 3.11 [#1072](https://github.com/pdm-project/pdm/issues/1072)\n- Drop the vendored libraries `click`, `halo`, `colorama` and `log_symbols`. PDM has no vendors now. [#1091](https://github.com/pdm-project/pdm/issues/1091)\n- Update dependency version `pdm-pep517` to `1.0.0`. [#1153](https://github.com/pdm-project/pdm/issues/1153)\n\n### Removals and Deprecations\n\n- PDM legacy metadata format(from `pdm 0.x`) is no longer supported. [#1157](https://github.com/pdm-project/pdm/issues/1157)\n\n### Miscellany\n\n- Provide a `tox.ini` file for easier local testing against all Python versions. [#1160](https://github.com/pdm-project/pdm/issues/1160)\n\n\nRelease v1.15.4 (2022-06-28)\n----------------------------\n\n### Bug Fixes\n\n- Revert #1106: Do not use `venv` scheme for `prefix` kind install scheme. [#1158](https://github.com/pdm-project/pdm/issues/1158)\n- Fix a bug when updating a package with extra requirements, the package version doesn't get updated correctly. [#1166](https://github.com/pdm-project/pdm/issues/1166)\n\n### Miscellany\n\n- Add additional installation option via [asdf-pdm](https://github.com/1oglop1/asdf-pdm).\n  Add `skip-add-to-path` option to installer in order to prevent changing `PATH`.\n  Replace `bin` variable name with `bin_dir`. [#1145](https://github.com/pdm-project/pdm/issues/1145)\n\n\nRelease v1.15.3 (2022-06-14)\n----------------------------\n\n### Bug Fixes\n\n- Fix a defect in the resolution preferences that causes an infinite resolution loop. [#1119](https://github.com/pdm-project/pdm/issues/1119)\n- Update the poetry importer to support the new `[tool.poetry.build]` config table. [#1131](https://github.com/pdm-project/pdm/issues/1131)\n\n### Improved Documentation\n\n- Add support for multiple versions of documentations. [#1126](https://github.com/pdm-project/pdm/issues/1126)\n\n\nRelease v1.15.2 (2022-06-06)\n----------------------------\n\n### Bug Fixes\n\n- Fix bug where SIGINT is sent to the main `pdm` process and not to the process actually being run. [#1095](https://github.com/pdm-project/pdm/issues/1095)\n- Fix a bug due to the build backend fallback, which causes different versions of the same requirement to exist in the build environment, making the building unstable depending on which version being used. [#1099](https://github.com/pdm-project/pdm/issues/1099)\n- Don't include the `version` in the cache key of the locked candidates if they are from a URL requirement. [#1099](https://github.com/pdm-project/pdm/issues/1099)\n- Fix a bug where dependencies with `requires-python` pre-release versions caused `pdm update` to fail with `InvalidPyVersion`. [#1111](https://github.com/pdm-project/pdm/issues/1111)\n\n\nRelease v1.15.1 (2022-06-02)\n----------------------------\n\n### Bug Fixes\n\n- Fix a bug that dependencies are missing from the dep graph when they are depended by a requirement with extras. [#1097](https://github.com/pdm-project/pdm/issues/1097)\n- Give a default version if the version is dynamic in `setup.cfg` or `setup.py`. [#1101](https://github.com/pdm-project/pdm/issues/1101)\n- Fix a bug that the hashes for file URLs are not included in the lock file. [#1103](https://github.com/pdm-project/pdm/issues/1103)\n- Fix a bug that package versions are updated even when they are excluded by `pdm update` command. [#1104](https://github.com/pdm-project/pdm/issues/1104)\n- Prefer `venv` install scheme when available. This scheme is more stable than `posix_prefix` scheme since the latter is often patched by distributions. [#1106](https://github.com/pdm-project/pdm/issues/1106)\n\n### Miscellany\n\n- Move the test artifacts to a submodule. It will make it easier to package this project. [#1084](https://github.com/pdm-project/pdm/issues/1084)\n\n\nRelease v1.15.0 (2022-05-16)\n----------------------------\n\n### Features & Improvements\n\n- Allow specifying lockfile other than `pdm.lock` by `--lockfile` option or `PDM_LOCKFILE` env var. [#1038](https://github.com/pdm-project/pdm/issues/1038)\n\n### Bug Fixes\n\n- Replace the editable entry in `pyproject.toml` when running `pdm add --no-editable <package>`. [#1050](https://github.com/pdm-project/pdm/issues/1050)\n- Ensure the pip module inside venv in installation script. [#1053](https://github.com/pdm-project/pdm/issues/1053)\n- Fix the py2 compatibility issue in the in-process `get_sysconfig_path.py` script. [#1056](https://github.com/pdm-project/pdm/issues/1056)\n- Fix a bug that file paths in URLs are not correctly unquoted. [#1073](https://github.com/pdm-project/pdm/issues/1073)\n- Fix a bug on Python 3.11 that overriding an existing command from plugins raises an error. [#1075](https://github.com/pdm-project/pdm/issues/1075)\n- Replace the `${PROJECT_ROOT}` variable in the result of `export` command. [#1079](https://github.com/pdm-project/pdm/issues/1079)\n\n### Removals and Deprecations\n\n- Show a warning if Python 2 interpreter is being used and remove the support on 2.0. [#1082](https://github.com/pdm-project/pdm/issues/1082)\n\n\nRelease v1.14.1 (2022-04-21)\n----------------------------\n\n### Features & Improvements\n\n- Ask for description when doing `pdm init` and create default README for libraries. [#1041](https://github.com/pdm-project/pdm/issues/1041)\n\n### Bug Fixes\n\n- Fix a bug of missing subdirectory fragment when importing from a `requirements.txt`. [#1036](https://github.com/pdm-project/pdm/issues/1036)\n- Fix use_cache.json with corrupted python causes `pdm use` error. [#1039](https://github.com/pdm-project/pdm/issues/1039)\n- Ignore the `optional` key when converting from Poetry's dependency entries. [#1042](https://github.com/pdm-project/pdm/issues/1042)\n\n### Improved Documentation\n\n- Clarify documentation on enabling PEP582 globally. [#1033](https://github.com/pdm-project/pdm/issues/1033)\n\n\nRelease v1.14.0 (2022-04-08)\n----------------------------\n\n### Features & Improvements\n\n- Editable installations won't be overridden unless `--no-editable` is passed.\n  `pdm add --no-editable` will now override the `editable` mode of the given packages. [#1011](https://github.com/pdm-project/pdm/issues/1011)\n- Re-calculate the file hashes when running `pdm lock --refresh`. [#1019](https://github.com/pdm-project/pdm/issues/1019)\n\n### Bug Fixes\n\n- Fix a bug that requirement with extras isn't resolved to the version as specified by the range. [#1001](https://github.com/pdm-project/pdm/issues/1001)\n- Replace the `${PROJECT_ROOT}` in the output of `pdm list`. [#1004](https://github.com/pdm-project/pdm/issues/1004)\n- Further fix the python path issue of macOS system installed Python. [#1023](https://github.com/pdm-project/pdm/issues/1023)\n- Fix the install path issue on Python 3.10 installed from homebrew. [#996](https://github.com/pdm-project/pdm/issues/996)\n\n### Improved Documentation\n\n- Document how to install PDM inside a project with Pyprojectx. [#1004](https://github.com/pdm-project/pdm/issues/1004)\n\n### Dependencies\n\n- Support `installer 0.5.x`. [#1002](https://github.com/pdm-project/pdm/issues/1002)\n\n\nRelease v1.13.6 (2022-03-28)\n----------------------------\n\n### Bug Fixes\n\n- Default the optional `license` field to \"None\". [#991](https://github.com/pdm-project/pdm/issues/991)\n- Don't create project files in `pdm search` command. [#993](https://github.com/pdm-project/pdm/issues/993)\n- Fix a bug that the env vars in source urls in exported result are not expanded. [#997](https://github.com/pdm-project/pdm/issues/997)\n\n\nRelease v1.13.5 (2022-03-23)\n----------------------------\n\n### Features & Improvements\n\n- Users can change the install destination of global project to the user site(`~/.local`) with `global_project.user_site` config. [#885](https://github.com/pdm-project/pdm/issues/885)\n- Make the path to the global project configurable. Rename the configuration `auto_global` to `global_project.fallback` and deprecate the old name. [#986](https://github.com/pdm-project/pdm/issues/986)\n\n### Bug Fixes\n\n- Fix the compatibility when fetching license information in `show` command. [#966](https://github.com/pdm-project/pdm/issues/966)\n- Don't follow symlinks for the paths in the requirement strings. [#976](https://github.com/pdm-project/pdm/issues/976)\n- Use the default install scheme when installing build requirements. [#983](https://github.com/pdm-project/pdm/issues/983)\n- Fix a bug that `_.site_packages` is overridden by default option value. [#985](https://github.com/pdm-project/pdm/issues/985)\n\n\nRelease v1.13.4 (2022-03-09)\n----------------------------\n\n### Features & Improvements\n\n- Update the dependency `pdm-pep517` to support PEP 639. [#959](https://github.com/pdm-project/pdm/issues/959)\n\n### Bug Fixes\n\n- Filter out the unmatched python versions when listing the available versions. [#941](https://github.com/pdm-project/pdm/issues/941)\n- Fix a bug displaying the available python versions. [#943](https://github.com/pdm-project/pdm/issues/943)\n- Fix a bug under non-UTF8 console encoding. [#960](https://github.com/pdm-project/pdm/issues/960)\n- Fix a bug that data files are not copied to the destination when using installation cache. [#961](https://github.com/pdm-project/pdm/issues/961)\n\n\nRelease v1.13.3 (2022-02-24)\n----------------------------\n\n### Bug Fixes\n\n- Fix a bug that VCS repo name are parsed as the package name. [#928](https://github.com/pdm-project/pdm/issues/928)\n- Support prerelease versions for global projects. [#932](https://github.com/pdm-project/pdm/issues/932)\n- Fix a bug that VCS revision in the lock file isn't respected when installing. [#933](https://github.com/pdm-project/pdm/issues/933)\n\n### Dependencies\n\n- Switch from `pythonfinder` to `findpython` as the Python version finder. [#930](https://github.com/pdm-project/pdm/issues/930)\n\n\nRelease v1.13.2 (2022-02-20)\n----------------------------\n\n### Bug Fixes\n\n- Fix a regression issue that prereleases can't be installed if the version specifier of the requirement doesn't imply that. [#920](https://github.com/pdm-project/pdm/issues/920)\n\n\nRelease v1.13.1 (2022-02-18)\n----------------------------\n\n### Bug Fixes\n\n- Fix a bug that bad pip cache dir value breaks PDM's check update function. [#922](https://github.com/pdm-project/pdm/issues/922)\n- Fix a race condition in parallel installation by changing metadata to a lazy property.\n  This fixes a bug that incompatible wheels are installed unexpectedly. [#924](https://github.com/pdm-project/pdm/issues/924)\n\n\nRelease v1.13.0.post0 (2022-02-18)\n----------------------------------\n\n### Bug Fixes\n\n- Fix a bug that incompatible platform-specific wheels are installed. [#921](https://github.com/pdm-project/pdm/issues/921)\n\n\nRelease v1.13.0 (2022-02-18)\n----------------------------\n\n### Features & Improvements\n\n- Support `pre_*` and `post_*` scripts for task composition. Pre- and Post- scripts for `init`, `build`, `install` and `lock` will be run if present. [#789](https://github.com/pdm-project/pdm/issues/789)\n- Support `--config/-c` option to specify another global configuration file. [#883](https://github.com/pdm-project/pdm/issues/883)\n- Packages with extras require no longer inherit the dependencies from the same package without extras. It is because the package without extras are returned as one of the dependencies. This change won't break the existing lock files nor dependency cache. [#892](https://github.com/pdm-project/pdm/issues/892)\n- Support version ranges in `[tool.pdm.overrides]` table. [#909](https://github.com/pdm-project/pdm/issues/909)\n- Rename config `use_venv` to `python.use_venv`;\n  rename config `feature.install_cache` to `install.cache`;\n  rename config `feature.install_cache_method` to `install.cache_method`;\n  rename config `parallel_install` to `install.parallel`. [#914](https://github.com/pdm-project/pdm/issues/914)\n\n### Bug Fixes\n\n- Fix a bug that file URLs or VCS URLs don't work in `[tool.pdm.overrides]` table. [#861](https://github.com/pdm-project/pdm/issues/861)\n- Fix a bug of identifier mismatch for URL requirements without an explicit name. [#901](https://github.com/pdm-project/pdm/issues/901)\n- No `requires-python` should be produced if ANY(`*`) is given. [#917](https://github.com/pdm-project/pdm/issues/917)\n- Fix a bug that `pdm.lock` gets created when `--dry-run` is passed to `pdm add`. [#918](https://github.com/pdm-project/pdm/issues/918)\n\n### Improved Documentation\n\n- The default editable backend becomes `path`. [#904](https://github.com/pdm-project/pdm/issues/904)\n\n### Removals and Deprecations\n\n- Stop auto-migrating projects from PDM 0.x format. [#912](https://github.com/pdm-project/pdm/issues/912)\n\n### Refactor\n\n- Rename `ExtrasError` to `ExtrasWarning` for better understanding. Improve the warning message. [#892](https://github.com/pdm-project/pdm/issues/892)\n- Extract the environment related code from `Candidate` into a new class `PreparedCandidate`.\n  `Candidate` no longer holds an `Environment` instance. [#920](https://github.com/pdm-project/pdm/issues/920)\n\n\nRelease v1.12.8 (2022-02-06)\n----------------------------\n\n### Features & Improvements\n\n- Print the error and continue if a plugin fails to load. [#878](https://github.com/pdm-project/pdm/issues/878)\n\n### Bug Fixes\n\n- PDM now ignores configuration of uninstalled plugins. [#872](https://github.com/pdm-project/pdm/issues/872)\n- Fix the compatibility issue with `pip>=22.0`. [#875](https://github.com/pdm-project/pdm/issues/875)\n\n\nRelease v1.12.7 (2022-01-31)\n----------------------------\n\n### Features & Improvements\n\n- If no command is given to `pdm run`, it will run the Python REPL. [#856](https://github.com/pdm-project/pdm/issues/856)\n\n### Bug Fixes\n\n- Fix the hash calculation when generating `direct_url.json` for a local pre-built wheel. [#861](https://github.com/pdm-project/pdm/issues/861)\n- PDM no longer migrates project meta silently. [#867](https://github.com/pdm-project/pdm/issues/867)\n\n### Dependencies\n\n- Pin `pip<22.0`. [#874](https://github.com/pdm-project/pdm/issues/874)\n\n### Miscellany\n\n- Reduce the number of tests that require network, and mark the rest with `network` marker. [#858](https://github.com/pdm-project/pdm/issues/858)\n\n\nRelease v1.12.6 (2022-01-12)\n----------------------------\n\n### Bug Fixes\n\n- Fix a bug that cache dir isn't created. [#843](https://github.com/pdm-project/pdm/issues/843)\n\n\nRelease v1.12.5 (2022-01-11)\n----------------------------\n\n### Bug Fixes\n\n- Fix a resolution error that dots in the package name are normalized to `-` unexpectedly. [#853](https://github.com/pdm-project/pdm/issues/853)\n\n\nRelease v1.12.4 (2022-01-11)\n----------------------------\n\n### Features & Improvements\n\n- Remember the last selection in `use` command to save the human effort.\n  And introduce an `-i` option to ignored that remembered value. [#846](https://github.com/pdm-project/pdm/issues/846)\n\n### Bug Fixes\n\n- Fix a bug of uninstall crash when the package has directories in `RECORD`. [#847](https://github.com/pdm-project/pdm/issues/847)\n- Fix the `ModuleNotFoundError` during uninstall when the modules required are removed. [#850](https://github.com/pdm-project/pdm/issues/850)\n\n\nRelease v1.12.3 (2022-01-07)\n----------------------------\n\n### Features & Improvements\n\n- Support setting Python path in global configuration. [#842](https://github.com/pdm-project/pdm/issues/842)\n\n### Bug Fixes\n\n- Lowercase the package names in the lock file make it more stable. [#836](https://github.com/pdm-project/pdm/issues/836)\n- Show the packages to be updated in dry run mode of `pdm update` even if `--no-sync` is passed. [#837](https://github.com/pdm-project/pdm/issues/837)\n- Improve the robustness of update check code. [#841](https://github.com/pdm-project/pdm/issues/841)\n- Fix a bug that export result has environment markers that don't apply for all requirements. [#843](https://github.com/pdm-project/pdm/issues/843)\n\n\nRelease v1.12.2 (2021-12-30)\n----------------------------\n\n### Features & Improvements\n\n- Allow changing the installation linking method by `feature.install_cache_method` config. [#822](https://github.com/pdm-project/pdm/issues/822)\n\n### Bug Fixes\n\n- Fix a bug that namespace packages can't be symlinked to the cache due to existing links. [#820](https://github.com/pdm-project/pdm/issues/820)\n- Make PDM generated pth files processed as early as possible. [#821](https://github.com/pdm-project/pdm/issues/821)\n- Fix a UnicodeDecodeError for subprocess logger under Windows/GBK. [#823](https://github.com/pdm-project/pdm/issues/823)\n\n\nRelease v1.12.1 (2021-12-24)\n----------------------------\n\n### Bug Fixes\n\n- Don't symlink pycaches to the target place. [#817](https://github.com/pdm-project/pdm/issues/817)\n\n\nRelease v1.12.0 (2021-12-22)\n----------------------------\n\n### Features & Improvements\n\n- Add `lock --refresh` to update the hash stored with the lock file without updating the pinned versions. [#642](https://github.com/pdm-project/pdm/issues/642)\n- Support resolution overriding in the `[tool.pdm.overrides]` table. [#790](https://github.com/pdm-project/pdm/issues/790)\n- Add support for signals for basic operations, now including `post_init`, `pre_lock`, `post_lock`, `pre_install` and `post_install`. [#798](https://github.com/pdm-project/pdm/issues/798)\n- Add `install --check` to check if the lock file is up to date. [#810](https://github.com/pdm-project/pdm/issues/810)\n- Use symlinks to cache installed packages when it is supported by the file system. [#814](https://github.com/pdm-project/pdm/issues/814)\n\n### Bug Fixes\n\n- Fix a bug that candidates from urls are rejected by the `allow_prereleases` setting.\n  Now non-named requirements are resolved earlier than pinned requirements. [#799](https://github.com/pdm-project/pdm/issues/799)\n\n### Improved Documentation\n\n- Add a new doc page: **API reference**. [#802](https://github.com/pdm-project/pdm/issues/802)\n\n### Dependencies\n\n- Switch back from `atoml` to `tomlkit` as the style-preserving TOML parser. The latter has supported TOML v1.0.0. [#809](https://github.com/pdm-project/pdm/issues/809)\n\n### Miscellany\n\n- Cache the latest version of PDM for one week to reduce the request frequency. [#800](https://github.com/pdm-project/pdm/issues/800)\n\n\nRelease v1.11.3 (2021-12-15)\n----------------------------\n\n### Features & Improvements\n\n- Change the default version save strategy to `minimum`, without upper bounds. [#787](https://github.com/pdm-project/pdm/issues/787)\n\n### Bug Fixes\n\n- Fix the patching of sysconfig in PEP 582 initialization script. [#796](https://github.com/pdm-project/pdm/issues/796)\n\n### Miscellany\n\n- Fix an installation failure of the bootstrap script on macOS Catalina. [#793](https://github.com/pdm-project/pdm/issues/793)\n- Add a basic benchmarking script. [#794](https://github.com/pdm-project/pdm/issues/794)\n\n\nRelease v1.11.2 (2021-12-10)\n----------------------------\n\n### Bug Fixes\n\n- Fix the resolution order to reduce the loop number to find a conflict. [#781](https://github.com/pdm-project/pdm/issues/781)\n- Patch the functions in `sysconfig` to return the PEP 582 scheme in `pdm run`. [#784](https://github.com/pdm-project/pdm/issues/784)\n\n### Dependencies\n\n- Remove the upper bound of version constraints for most dependencies, except for some zero-versioned ones. [#787](https://github.com/pdm-project/pdm/issues/787)\n\n\nRelease v1.11.1 (2021-12-08)\n----------------------------\n\n### Features & Improvements\n\n- Support `--pre/--prerelease` option for `pdm add` and `pdm update`. It will allow prereleases to be pinned. [#774](https://github.com/pdm-project/pdm/issues/774)\n- Improve the error message when python is found but not meeting the python requirement. [#777](https://github.com/pdm-project/pdm/issues/777)\n\n### Bug Fixes\n\n- Fix a bug that `git+https` candidates cannot be resolved. [#771](https://github.com/pdm-project/pdm/issues/771)\n- Fix an infinite resolution loop by resolving the top-level packages first. Also deduplicate the lines from the same requirement in the error output. [#776](https://github.com/pdm-project/pdm/issues/776)\n\n### Miscellany\n\n- Fix the install script to use a zipapp of virtualenv when it isn't installed. [#780](https://github.com/pdm-project/pdm/issues/780)\n\n\nRelease v1.11.0 (2021-11-30)\n----------------------------\n\n### Features & Improvements\n\n- Move `version` from `[project]` table to `[tool.pdm]` table, delete `classifiers` from `dynamic`, and warn usage about the deprecated usages. [#748](https://github.com/pdm-project/pdm/issues/748)\n- Add support for Conda environments in addition to Python virtual environments. [#749](https://github.com/pdm-project/pdm/issues/749)\n- Add support for saving only the lower bound `x >= VERSION` when adding dependencies. [#752](https://github.com/pdm-project/pdm/issues/752)\n- Improve the error message when resolution fails. [#754](https://github.com/pdm-project/pdm/issues/754)\n\n### Bug Fixes\n\n- Switch to self-implemented `pdm list --freeze` to fix a bug due to Pip's API change. [#533](https://github.com/pdm-project/pdm/issues/533)\n- Fix an infinite loop issue when resolving candidates with incompatible `requires-python`. [#744](https://github.com/pdm-project/pdm/issues/744)\n- Fix the python finder to support pyenv-win. [#745](https://github.com/pdm-project/pdm/issues/745)\n- Fix the ANSI color output for Windows cmd and Powershell terminals. [#753](https://github.com/pdm-project/pdm/issues/753)\n\n### Removals and Deprecations\n\n- Remove `-s/--section` option from all previously supported commands. Use `-G/--group` instead. [#756](https://github.com/pdm-project/pdm/issues/756)\n\n\nRelease v1.10.3 (2021-11-18)\n----------------------------\n\n### Bug Fixes\n\n- Use `importlib` to replace `imp` in the `sitecustomize` module for Python 3. [#574](https://github.com/pdm-project/pdm/issues/574)\n- Fix the lib paths under non-isolated build. [#740](https://github.com/pdm-project/pdm/issues/740)\n- Exclude the dependencies with extras in the result of `pdm export`. [#741](https://github.com/pdm-project/pdm/issues/741)\n\n\nRelease v1.10.2 (2021-11-14)\n----------------------------\n\n### Features & Improvements\n\n- Add a new option `-s/--site-packages` to `pdm run` as well as a script config item. When it is set to `True`, site-packages from the selected interpreter will be loaded into the running environment. [#733](https://github.com/pdm-project/pdm/issues/733)\n\n### Bug Fixes\n\n- Now `NO_SITE_PACKAGES` isn't set in `pdm run` if the executable is out of local packages. [#733](https://github.com/pdm-project/pdm/issues/733)\n\n\nRelease v1.10.1 (2021-11-09)\n----------------------------\n\n### Features & Improvements\n\n- Isolate the project environment with system site packages in `pdm run`, but keep them seen when PEP 582 is enabled. [#708](https://github.com/pdm-project/pdm/issues/708)\n\n### Bug Fixes\n\n- Run `pip` with `--isolated` when building wheels. In this way some env vars like `PIP_REQUIRE_VIRTUALENV` can be ignored. [#669](https://github.com/pdm-project/pdm/issues/669)\n- Fix the install script to ensure `pip` is not DEBUNDLED. [#685](https://github.com/pdm-project/pdm/issues/685)\n- Fix a bug that when `summary` is `None`, the lockfile can't be generated. [#719](https://github.com/pdm-project/pdm/issues/719)\n- `${PROJECT_ROOT}` should be written in the URL when relative path is given. [#721](https://github.com/pdm-project/pdm/issues/721)\n- Fix a bug that when project table already exists, `pdm import` can't merge the settings correctly. [#723](https://github.com/pdm-project/pdm/issues/723)\n\n\nRelease v1.10.0 (2021-10-25)\n----------------------------\n\n### Features & Improvements\n\n- Add `--no-sync` option to `update` command. [#684](https://github.com/pdm-project/pdm/issues/684)\n- Support `find_links` source type. It can be specified via `type` key of `[[tool.pdm.source]]` table. [#694](https://github.com/pdm-project/pdm/issues/694)\n- Add `--dry-run` option to `add`, `install` and `remove` commands. [#698](https://github.com/pdm-project/pdm/issues/698)\n\n### Bug Fixes\n\n- Remove trailing whitespace with terminal output of tables (via `project.core.ui.display_columns`), fixing unnecessary wrapping due to / with empty lines full of spaces in case of long URLs in the last column. [#680](https://github.com/pdm-project/pdm/issues/680)\n- Include files should be installed under venv's base path. [#682](https://github.com/pdm-project/pdm/issues/682)\n- Ensure the value of `check_update` is boolean. [#689](https://github.com/pdm-project/pdm/issues/689)\n\n### Improved Documentation\n\n- Update the contributing guide, remove the usage of `setup_dev.py` in favor of `pip install`. [#676](https://github.com/pdm-project/pdm/issues/676)\n\n\nRelease v1.9.0 (2021-10-12)\n---------------------------\n\n### Bug Fixes\n\n- Fix a bug that `requires-python` is not recognized in candidates evaluation. [#657](https://github.com/pdm-project/pdm/issues/657)\n- Fix the path order when pdm run so that executables in local packages dir are found first. [#678](https://github.com/pdm-project/pdm/issues/678)\n\n### Dependencies\n\n- Update `installer` to `0.3.0`, fixing a bug that broke installation of some packages with unusual wheel files. [#653](https://github.com/pdm-project/pdm/issues/653)\n- Change `packaging` and `typing-extensions` to direct dependencies. [#674](https://github.com/pdm-project/pdm/issues/674)\n\n### Refactor\n\n- `requires-python` now participates in the resolution as a dummy requirement. [#658](https://github.com/pdm-project/pdm/issues/658)\n\n\nRelease v1.8.5 (2021-09-16)\n---------------------------\n\n### Bug Fixes\n\n- Fix the error of regex to find the shebang line. [#656](https://github.com/pdm-project/pdm/issues/656)\n\n\nRelease v1.8.4 (2021-09-15)\n---------------------------\n\n### Features & Improvements\n\n- Support `--no-isolation` option for `install`, `lock`, `update`, `remove`, `sync` commands. [#640](https://github.com/pdm-project/pdm/issues/640)\n- Make `project_max_depth` configurable and default to `5`. [#643](https://github.com/pdm-project/pdm/issues/643)\n\n### Bug Fixes\n\n- Don't try `pdm-pep517` backend on Python 2.7 when installing self as editable. [#640](https://github.com/pdm-project/pdm/issues/640)\n- Fix a bug that existing shebang can't be replaced correctly. [#651](https://github.com/pdm-project/pdm/issues/651)\n- Fix the version range saving for prerelease versions. [#654](https://github.com/pdm-project/pdm/issues/654)\n\n\nRelease v1.8.3 (2021-09-07)\n---------------------------\n\n### Features & Improvements\n\n- Allow to build in non-isolated environment, to enable optional speedups depending on the environment. [#635](https://github.com/pdm-project/pdm/issues/635)\n\n### Bug Fixes\n\n- Don't copy `*-nspkg.pth` files in `install_cache` mode. It will still work without them. [#623](https://github.com/pdm-project/pdm/issues/623)\n\n\nRelease v1.8.2 (2021-09-01)\n---------------------------\n\n### Bug Fixes\n\n- Fix the removal issue of standalone pyc files [#633](https://github.com/pdm-project/pdm/issues/633)\n\n\nRelease v1.8.1 (2021-08-26)\n---------------------------\n\n### Features & Improvements\n\n- Add `-r/--reinstall` option to `sync` command to force re-install the existing dependencies. [#601](https://github.com/pdm-project/pdm/issues/601)\n- Show update hint after every pdm command. [#603](https://github.com/pdm-project/pdm/issues/603)\n- `pdm cache clear` can clear cached installations if not needed any more. [#604](https://github.com/pdm-project/pdm/issues/604)\n\n### Bug Fixes\n\n- Fix the editable install script so that `setuptools` won't see the dependencies under local packages. [#601](https://github.com/pdm-project/pdm/issues/601)\n- Preserve the executable bit when installing wheels. [#606](https://github.com/pdm-project/pdm/issues/606)\n- Write PEP 610 metadata `direct_url.json` when installing wheels. [#607](https://github.com/pdm-project/pdm/issues/607)\n- Fix a bug that `*` fails to be converted as `SpecifierSet`. [#609](https://github.com/pdm-project/pdm/issues/609)\n\n### Refactor\n\n- Build editable packages are into wheels via PEP 660 build backend. Now all installations are unified into wheels. [#612](https://github.com/pdm-project/pdm/issues/612)\n\n\nRelease v1.8.0 (2021-08-16)\n---------------------------\n\n### Features & Improvements\n\n- Added a new mode `--json` to the list command which outputs the dependency graph as a JSON document. [#583](https://github.com/pdm-project/pdm/issues/583)\n- Add a new config `feature.install_cache`. When it is turned on, wheels will be installed into a centralized package repo and create `.pth` files under project packages directory to link to the cached package. [#589](https://github.com/pdm-project/pdm/issues/589)\n\n### Bug Fixes\n\n- Fix env vars in source URLs not being expanded in all cases. [#570](https://github.com/pdm-project/pdm/issues/570)\n- Fix the weird output of `pdm show`. [#580](https://github.com/pdm-project/pdm/issues/580)\n- Prefer `~/.pyenv/shims/python3` as the pyenv interpreter. [#590](https://github.com/pdm-project/pdm/issues/590)\n- Fix a bug that installing will download candidates that do not match the locked hashes. [#596](https://github.com/pdm-project/pdm/issues/596)\n\n### Improved Documentation\n\n- Added instructions to the Contributing section for creating news fragments [#573](https://github.com/pdm-project/pdm/issues/573)\n\n### Removals and Deprecations\n\n- Deprecate `-s/--section` option in favor of `-G/--group`. [#591](https://github.com/pdm-project/pdm/issues/591)\n\n### Refactor\n\n- Switch to a self-implemented version of uninstaller. [#586](https://github.com/pdm-project/pdm/issues/586)\n- `pdm/installers/installers.py` is renamed to `pdm/installers/manager.py` to be more accurate. The `Installer` class under that file is renamed to `InstallerManager` and is exposed in the `pdm.core.Core` object for overriding. The new `pdm/installers/installers.py` contains some installation implementations. [#589](https://github.com/pdm-project/pdm/issues/589)\n- Switch from `pkg_resources.Distribution` to the implementation of `importlib.metadata`. [#592](https://github.com/pdm-project/pdm/issues/592)\n\n\nRelease v1.7.2 (2021-07-30)\n---------------------------\n\n### Bug Fixes\n\n- Remove the existing files before installing. [#565](https://github.com/pdm-project/pdm/issues/565)\n- Deduplicate the plugins list. [#566](https://github.com/pdm-project/pdm/issues/566)\n\n\nRelease v1.7.1 (2021-07-29)\n---------------------------\n\n### Bug Fixes\n\n- Accept non-canonical distribution name in the wheel's dist-info directory name. [#529](https://github.com/pdm-project/pdm/issues/529)\n- Prefer requirements with narrower version constraints or allowing prereleases to find matches. [#551](https://github.com/pdm-project/pdm/issues/551)\n- Use the underlying real executable path for writing shebangs. [#553](https://github.com/pdm-project/pdm/issues/553)\n- Fix a bug that extra markers cannot be extracted when combined with other markers with \"and\". [#559](https://github.com/pdm-project/pdm/issues/559)\n- Fix a bug that redacted credentials in source urls get overwritten with the plain text after locking. [#561](https://github.com/pdm-project/pdm/issues/561)\n\n### Refactor\n\n- Use installer as the wheel installer, replacing `distlib`. [#519](https://github.com/pdm-project/pdm/issues/519)\n\n\nRelease v1.7.0 (2021-07-20)\n---------------------------\n\n### Features & Improvements\n\n- Support showing individual fields by `--<field-name>` options in pdm show. When no package is given, show this project. [#527](https://github.com/pdm-project/pdm/issues/527)\n- Add `--freeze` option to `pdm list` command which shows the dependencies list as pip's requirements.txt format. [#531](https://github.com/pdm-project/pdm/issues/531)\n\n### Bug Fixes\n\n- Fix the path manipulation on Windows, now the PEP 582 path is prepended to the `PYTHONPATH`. [#522](https://github.com/pdm-project/pdm/issues/522)\n- Fix the handling of auth prompting: will try keyring in non-verbose mode. [#523](https://github.com/pdm-project/pdm/issues/523)\n- Recognize old entry point name \"pdm.plugin\" for backward-compatibility. [#530](https://github.com/pdm-project/pdm/issues/530)\n- Match the VCS scheme in case-insensitive manner. [#537](https://github.com/pdm-project/pdm/issues/537)\n- Use the default permission bits when writing project files. [#542](https://github.com/pdm-project/pdm/issues/542)\n- Fix the VCS url to be consistent between lock and install. [#547](https://github.com/pdm-project/pdm/issues/547)\n\n### Improved Documentation\n\n- Add installation instructions for Scoop. [#522](https://github.com/pdm-project/pdm/issues/522)\n\n### Dependencies\n\n- Update `pdm-pep517` to `0.8.0`. [#524](https://github.com/pdm-project/pdm/issues/524)\n- Switch from `toml` to `tomli`. [#541](https://github.com/pdm-project/pdm/issues/541)\n\n### Refactor\n\n- Separate the build env into two different levels for better caching. [#541](https://github.com/pdm-project/pdm/issues/541)\n- Refactor the build part into smaller functions. [#543](https://github.com/pdm-project/pdm/issues/543)\n\n\nRelease v1.6.4 (2021-06-23)\n---------------------------\n\n### Features & Improvements\n\n- Extract package name from egg-info in filename when eligible. Remove the patching code of resolvelib's inner class. [#441](https://github.com/pdm-project/pdm/issues/441)\n- Support installing packages from subdirectories of VCS repository. [#507](https://github.com/pdm-project/pdm/issues/507)\n- Add an install script to bootstrap PDM quickly without help of other tools. Modify docs to recommend this installation method. [#508](https://github.com/pdm-project/pdm/issues/508)\n- Add a new subcommand `plugin` to manage pdm plugins, including `add`, `remove` and `list` commands. [#510](https://github.com/pdm-project/pdm/issues/510)\n\n### Bug Fixes\n\n- Don't monkeypatch the internal class of `resolvelib` any more. This makes PDM more stable across updates of sub-dependencies. [#515](https://github.com/pdm-project/pdm/issues/515)\n\n### Miscellany\n\n- Clear the type errors from mypy. [#261](https://github.com/pdm-project/pdm/issues/261)\n\n\nRelease v1.6.3 (2021-06-17)\n---------------------------\n\n### Features & Improvements\n\n- Add an option `-u/--unconstrained` to support unconstraining version specifiers when adding packages. [#501](https://github.com/pdm-project/pdm/issues/501)\n\n### Bug Fixes\n\n- Fix the format of dependency arrays when a new value is appended. [#487](https://github.com/pdm-project/pdm/issues/487)\n- Allow missing email attribute for authors and maintainers. [#492](https://github.com/pdm-project/pdm/issues/492)\n- Fix a bug that editable install shouldn't require pyproject.toml to be valid. [#497](https://github.com/pdm-project/pdm/issues/497)\n- Fix a bug on macOS that purelib and platlib paths of isolated build envs cannot be substituted correctly if the Python is a framework build. [#502](https://github.com/pdm-project/pdm/issues/502)\n- Fix the version sort of candidates. [#506](https://github.com/pdm-project/pdm/issues/506)\n\n\nRelease v1.6.2 (2021-05-31)\n---------------------------\n\nNo significant changes.\n\n\nRelease v1.6.1 (2021-05-31)\n---------------------------\n\nNo significant changes.\n\n\nRelease v1.6.0 (2021-05-31)\n---------------------------\n\n### Features & Improvements\n\n- Use a new approach to determine the packages to be installed. This requires a quick resolution step before installation. [#456](https://github.com/pdm-project/pdm/issues/456)\n- `pdm export` no longer produces requirements file applicable for all platforms due to the new approach. [#456](https://github.com/pdm-project/pdm/issues/456)\n- Add structural typing for requirements module. Refactor the requirements module for that purpose. [#433](https://github.com/pdm-project/pdm/issues/433)\n- Introduce `--no-editable` option to install non-editable versions of all packages. [#443](https://github.com/pdm-project/pdm/issues/443)\n- Introduce `--no-self` option to prevent the project itself from being installed. [#444](https://github.com/pdm-project/pdm/issues/444)\n- Add a default `.gitignore` file in the `__pypackages__` directory. [#446](https://github.com/pdm-project/pdm/issues/446)\n- Check if the lock file version is compatible with PDM program before installation. [#463](https://github.com/pdm-project/pdm/issues/463)\n- Expose the project root path via `PDM_PROJECT_ROOT` env var. Change to the project root when executing scripts. [#470](https://github.com/pdm-project/pdm/issues/470)\n- Fix a bug that installation resolution doesn't respect the requirement markers from pyproject config. [#480](https://github.com/pdm-project/pdm/issues/480)\n\n### Bug Fixes\n\n- Changing to multiline breaks the parsing of TOML document. [#462](https://github.com/pdm-project/pdm/issues/462)\n- Fix a bug that transient dependencies of conditional requirements can't be resolved. [#472](https://github.com/pdm-project/pdm/issues/472)\n- Fix a bug that invalid wheels are rejected while they are acceptable for resolution. [#473](https://github.com/pdm-project/pdm/issues/473)\n- Fix a bug that build environment is not fully isolated with the hosted environment. [#477](https://github.com/pdm-project/pdm/issues/477)\n- Ensure the lock file is compatible before looking for the locked candidates. [#484](https://github.com/pdm-project/pdm/issues/484)\n\n### Improved Documentation\n\n- Fix 404 links in documentation. [#472](https://github.com/pdm-project/pdm/issues/472)\n\n### Dependencies\n\n- Migrate from `tomlkit` to `atoml` as the style-preserving TOML parser and writer. [#465](https://github.com/pdm-project/pdm/issues/465)\n\n### Removals and Deprecations\n\n- Remove the warning of `--dev` flag for older versions of PDM. [#444](https://github.com/pdm-project/pdm/issues/444)\n\n### Miscellany\n\n- Add Python 3.10 beta CI job. [#457](https://github.com/pdm-project/pdm/issues/457)\n\n\nRelease v1.5.3 (2021-05-10)\n---------------------------\n\n### Features & Improvements\n\n- Support passing options to the build backends via `--config-setting`. [#452](https://github.com/pdm-project/pdm/issues/452)\n\n### Bug Fixes\n\n- Seek for other sitecustomize.py to import. [#422](https://github.com/pdm-project/pdm/issues/422)\n- Fix an unescaped single quote in fish completion script. [#423](https://github.com/pdm-project/pdm/issues/423)\n- The hashes of a remote file candidate should be calculated from the link itself. [#450](https://github.com/pdm-project/pdm/issues/450)\n\n### Dependencies\n\n- Remove `keyring` as a dependency and guide users to install it when it is not available. [#442](https://github.com/pdm-project/pdm/issues/442)\n- Specify the minimum version of `distlib`. [#447](https://github.com/pdm-project/pdm/issues/447)\n\n### Miscellany\n\n- Add log output about found candidates and their origin. [#421](https://github.com/pdm-project/pdm/issues/421)\n- Add [mypy](https://github.com/python/mypy) pre-commit hook [#427](https://github.com/pdm-project/pdm/issues/427)\n- Improve type safety of `pdm.cli.actions` [#428](https://github.com/pdm-project/pdm/issues/428)\n- Fix wrong mypy configuration. [#451](https://github.com/pdm-project/pdm/issues/451)\n\n\nRelease v1.5.2 (2021-04-27)\n---------------------------\n\n### Features & Improvements\n\n- Allow `pdm use` with no argument given, which will list all available pythons for pick. [#409](https://github.com/pdm-project/pdm/issues/409)\n\n### Bug Fixes\n\n- Inform user to enable PEP 582 for development script to work. [#404](https://github.com/pdm-project/pdm/issues/404)\n- Check the existence of pyenv shim Python interpreter before using it. [#406](https://github.com/pdm-project/pdm/issues/406)\n- Fix a bug that executing `setup.py` failed for NameError. [#407](https://github.com/pdm-project/pdm/issues/407)\n- Check before setting the PYTHONPATH environment variable for PEP582 [#410](https://github.com/pdm-project/pdm/issues/410)\n- Fix development setup error. [#415](https://github.com/pdm-project/pdm/issues/415)\n\n### Dependencies\n\n- Update pip to 21.1 and fix compatibility issues. [#412](https://github.com/pdm-project/pdm/issues/412)\n\n\nRelease v1.5.1 (2021-04-22)\n---------------------------\n\n### Bug Fixes\n\n- Make func translate_sections pure to avoid exporting requirements in random order. [#401](https://github.com/pdm-project/pdm/issues/401)\n- Expand the variables in install requirements' attributes for build. [#402](https://github.com/pdm-project/pdm/issues/402)\n\n\nRelease v1.5.0 (2021-04-20)\n---------------------------\n\n### Features & Improvements\n\n- Include dev dependencies by default for `install` and `sync` commands. Add a new option `--prod/--production` to exclude them. Improve the dependency selection logic to be more convenient to use — the more common the usage is, the shorter the command is. [#391](https://github.com/pdm-project/pdm/issues/391)\n\n### Bug Fixes\n\n- Enquote executable path to ensure generating valid scripts. [#387](https://github.com/pdm-project/pdm/issues/387)\n- Consider hashes when fetching artifact link for build. [#389](https://github.com/pdm-project/pdm/issues/389)\n- Consider the sources settings when building. [#399](https://github.com/pdm-project/pdm/issues/399)\n\n### Improved Documentation\n\n- New pdm setting `source-includes` to mark files to be included only in sdist builds. [#390](https://github.com/pdm-project/pdm/issues/390)\n\n### Dependencies\n\n- Update `pdm-pep517` to `0.7.0`; update `resolvelib` to` 0.7.0`. [#390](https://github.com/pdm-project/pdm/issues/390)\n\n### Removals and Deprecations\n\n- Deprecate the usage of `-d/--dev` option in `install` and `sync` commands. [#391](https://github.com/pdm-project/pdm/issues/391)\n\n\nRelease v1.5.0b1 (2021-04-12)\n-----------------------------\n\n### Features & Improvements\n\n- Improve the env builder to run in isolated mode. [#384](https://github.com/pdm-project/pdm/issues/384)\n\n### Bug Fixes\n\n- Remove the incompatible code from the files that will be run in-process. [#375](https://github.com/pdm-project/pdm/issues/375)\n- Get the correct Python ABI tag of selected interpreter [#378](https://github.com/pdm-project/pdm/issues/378)\n- Error out when doing `pdm run` on a directory not initialized yet.\n- Give warning message when the project automatically fallbacks to the global project.\n\n### Dependencies\n\n- Upgrade `resolvelib` to `0.6.0`. [#381](https://github.com/pdm-project/pdm/issues/381)\n\n### Miscellany\n\n- refactor `pdm.models.readers` to improve typing support [#321](https://github.com/pdm-project/pdm/issues/321)\n- Add a basic integration test for cross-python check. [#377](https://github.com/pdm-project/pdm/issues/377)\n- Refactor the `project.python_executable` to `project.python` that contains all info of the interpreter. [#382](https://github.com/pdm-project/pdm/issues/382)\n- Continue refactoring Python info to extract to its own module. [#383](https://github.com/pdm-project/pdm/issues/383)\n- Refactor the creation of project.\n\n\nRelease v1.5.0b0 (2021-04-03)\n-----------------------------\n\n### Features & Improvements\n\n- Add hand-written zsh completion script. [#188](https://github.com/pdm-project/pdm/issues/188)\n- Add a special value `:all` given to `-s/--section` to refer to all sections under the same species.\n  Adjust `add`, `sync`, `install`, `remove` and `update` to support the new `dev-dependencies` groups. Old behavior will be kept the same. [#351](https://github.com/pdm-project/pdm/issues/351)\n- `dev-dependencies` is now a table of dependencies groups, where key is the group name and value is an array of dependencies. These dependencies won't appear in the distribution's metadata. `dev-dependencies` of the old format will turn into `dev` group under `dev-dependencies`. [#351](https://github.com/pdm-project/pdm/issues/351)\n- Move `dev-dependencies`, `includes`, `excludes` and `package-dir` out from `[project]` table to `[tool.pdm]` table. The migration will be done automatically if old format is detected. [#351](https://github.com/pdm-project/pdm/issues/351)\n- Throws an error with meaningful message when no candidate is found for one requirement. [#357](https://github.com/pdm-project/pdm/issues/357)\n- Support `--dry-run` option for `update` command to display packages that need update, install or removal. Add `--top` option to limit to top level packages only. [#358](https://github.com/pdm-project/pdm/issues/358)\n- Full-featured completion scripts for Zsh and Powershell - section selection, package name autocompletion and so on. Windows is a first-class citizen! [#367](https://github.com/pdm-project/pdm/issues/367)\n- Support non-interactive `init` command via `-n/--non-interactive` option. No question will be asked in this mode. [#368](https://github.com/pdm-project/pdm/issues/368)\n- Show project packages path(PEP 582) in the output of `pdm info`, also add an option `--packages` to show that value only. [#372](https://github.com/pdm-project/pdm/issues/372)\n\n### Bug Fixes\n\n- Fix a bug that pure python libraries are not loaded to construct the WorkingSet. [#346](https://github.com/pdm-project/pdm/issues/346)\n- Don't write `<script>-X.Y` variant to the bin folder. [#365](https://github.com/pdm-project/pdm/issues/365)\n- Python is now run in isolated mode via subprocess to avoid accidentally importing user packages. [#369](https://github.com/pdm-project/pdm/issues/369)\n- Don't overwrite existing dependencies when importing from requirements.txt. [#370](https://github.com/pdm-project/pdm/issues/370)\n\n### Improved Documentation\n\n- Add instructions of how to integrate PDM with Emacs, contributed by @linw1995. [#372](https://github.com/pdm-project/pdm/issues/372)\n\n### Removals and Deprecations\n\n- Remove the support of project path following `-g/--global` that was deprecated in `1.4.0`. One should use `-g -p <project_path>` for that purpose. [#361](https://github.com/pdm-project/pdm/issues/361)\n\n### Miscellany\n\n- Add test coverage to PDM. [#109](https://github.com/pdm-project/pdm/issues/109)\n- Add type annotations into untyped functions to start using mypy. [#354](https://github.com/pdm-project/pdm/issues/354)\n- Refactor the format converter code to be more explicit. [#360](https://github.com/pdm-project/pdm/issues/360)\n\n\nRelease v1.4.5 (2021-03-30)\n---------------------------\n\n### Features & Improvements\n\n- Skip the first prompt of `pdm init` [#352](https://github.com/pdm-project/pdm/issues/352)\n\n### Bug Fixes\n\n- Fix a test failure when using homebrew installed python. [#348](https://github.com/pdm-project/pdm/issues/348)\n- Get revision from the VCS URL if source code isn't downloaded to local. [#349](https://github.com/pdm-project/pdm/issues/349)\n\n### Dependencies\n\n- Update dependency `pdm-pep517` to `0.6.1`. [#353](https://github.com/pdm-project/pdm/issues/353)\n\n\nRelease v1.4.4 (2021-03-27)\n---------------------------\n\n### Features & Improvements\n\n- Emit warning if version or description can't be retrieved when importing from flit metadata. [#342](https://github.com/pdm-project/pdm/issues/342)\n- Add `type` argument to `pdm cache clear` and improve its UI. [#343](https://github.com/pdm-project/pdm/issues/343)\n- Always re-install the editable packages when syncing the working set. This can help tracking the latest change of `entry-points`. [#344](https://github.com/pdm-project/pdm/issues/344)\n\n### Bug Fixes\n\n- Make installer quit early if a wheel isn't able to build. [#338](https://github.com/pdm-project/pdm/issues/338)\n\n### Miscellany\n\n- ignore type checking in `models.project_info.ProjectInfo`, which indexes `distlib.metadata._data` [#335](https://github.com/pdm-project/pdm/issues/335)\n\n\nRelease v1.4.3 (2021-03-24)\n---------------------------\n\n### Features & Improvements\n\n- Change the group name of entry points from `pdm.plugins` to `pdm`.\n  Export some useful objects and models for shorter import path. [#318](https://github.com/pdm-project/pdm/issues/318)\n- Field `cmd` in `tools.pdm.scripts` configuration items now allows specifying an argument array instead of a string.\n- Refactor: Remove the reference of `stream` singleton, improve the UI related code. [#320](https://github.com/pdm-project/pdm/issues/320)\n- Support dependencies managed by poetry and flit being installed as editable packages. [#324](https://github.com/pdm-project/pdm/issues/324)\n- Refactor: Extract the logic of finding interpreters to method for the sake of subclass overriding. [#326](https://github.com/pdm-project/pdm/issues/326)\n- Complete the `cache` command, add `list`, `remove` and `info` subcommands. [#329](https://github.com/pdm-project/pdm/issues/329)\n- Refactor: Unify the code about selecting interpreter to reduce the duplication. [#331](https://github.com/pdm-project/pdm/issues/331)\n- Retrieve the version and description of a flit project by parsing the AST of the main file. [#333](https://github.com/pdm-project/pdm/issues/333)\n\n### Bug Fixes\n\n- Fix a parsing error when non-ascii characters exist in `pyproject.toml`. [#308](https://github.com/pdm-project/pdm/issues/308)\n- Fix a bug that non-editable VCS candidates can't satisfy their requirements once locked in the lock file. [#314](https://github.com/pdm-project/pdm/issues/314)\n- Fix a bug of import-on-init that fails when requirements.txt is detected. [#328](https://github.com/pdm-project/pdm/issues/328)\n\n### Miscellany\n\n- refactor `pdm.iostream` to improve 'typing' support [#301](https://github.com/pdm-project/pdm/issues/301)\n- fix some typos [#323](https://github.com/pdm-project/pdm/issues/323)\n\n\nRelease v1.4.2 (2021-03-18)\n---------------------------\n\n### Features & Improvements\n\n- Refactor the code, extract the version related logic from `specifiers.py` to a separated module. [#303](https://github.com/pdm-project/pdm/issues/303)\n\n### Bug Fixes\n\n- Fix a bug that get_dependencies() returns error when the `setup.py` has no `install_requires` key. [#299](https://github.com/pdm-project/pdm/issues/299)\n- Pin the VCS revision for non-editable VCS candidates in the lock file. [#305](https://github.com/pdm-project/pdm/issues/305)\n- Fix a bug that editable build hits the cached wheel unexpectedly. [#307](https://github.com/pdm-project/pdm/issues/307)\n\n### Miscellany\n\n- replace 'typing comments' with type annotations throughout [#298](https://github.com/pdm-project/pdm/issues/298)\n\n\nRelease v1.4.1 (2021-03-12)\n---------------------------\n\n### Features & Improvements\n\n- Support importing dependencies from requirements.txt to dev-dependencies or sections. [#291](https://github.com/pdm-project/pdm/issues/291)\n\n### Bug Fixes\n\n- Fallback to static parsing when building was failed to find the dependencies of a candidate. [#293](https://github.com/pdm-project/pdm/issues/293)\n- Fix a bug that `pdm init` fails when `pyproject.toml` exists but has no `[project]` section. [#295](https://github.com/pdm-project/pdm/issues/295)\n\n### Improved Documentation\n\n- Document about how to use PDM with Nox. [#281](https://github.com/pdm-project/pdm/issues/281)\n\n\nRelease v1.4.0 (2021-03-05)\n---------------------------\n\n### Features & Improvements\n\n- When `-I/--ignore-python` passed or `PDM_IGNORE_SAVED_PYTHON=1`, ignore the interpreter set in `.pdm.toml` and don't save to it afterwards. [#283](https://github.com/pdm-project/pdm/issues/283)\n- A new option `-p/--project` is introduced to specify another path for the project base. It can also be combined with `-g/--global` option.\n  The latter is changed to a flag only option that does not accept values. [#286](https://github.com/pdm-project/pdm/issues/286)\n- Support `-f setuppy` for `pdm export` to export the metadata as setup.py [#289](https://github.com/pdm-project/pdm/issues/289)\n\n### Bug Fixes\n\n- Fix a bug that editable local package requirements cannot be parsed rightly. [#285](https://github.com/pdm-project/pdm/issues/285)\n- Change the priority of metadata files to parse so that PEP 621 metadata will be parsed first. [#288](https://github.com/pdm-project/pdm/issues/288)\n\n### Improved Documentation\n\n- Add examples of how to integrate with CI pipelines (and tox). [#281](https://github.com/pdm-project/pdm/issues/281)\n\n\nRelease v1.3.4 (2021-03-01)\n---------------------------\n\n### Improved Documentation\n\n- added documentation on a [task provider for vscode](https://marketplace.visualstudio.com/items?itemName=knowsuchagency.pdm-task-provider) [#280](https://github.com/pdm-project/pdm/issues/280)\n\n### Bug Fixes\n\n- Ignore the python requires constraints when fetching the link from the PyPI index.\n\nRelease v1.3.3 (2021-02-26)\n---------------------------\n\n### Bug Fixes\n\n- Fix the requirement string of a VCS requirement to comply with PEP 508. [#275](https://github.com/pdm-project/pdm/issues/275)\n- Fix a bug that editable packages with `src` directory can't be uninstalled correctly. [#277](https://github.com/pdm-project/pdm/issues/277)\n- Fix a bug that editable package doesn't override the non-editable version in the working set. [#278](https://github.com/pdm-project/pdm/issues/278)\n\n\nRelease v1.3.2 (2021-02-25)\n---------------------------\n\n### Features & Improvements\n\n- Abort and tell user the selected section following `pdm sync` or `pdm install` is not present in the error message. [#274](https://github.com/pdm-project/pdm/issues/274)\n\n### Bug Fixes\n\n- Fix a bug that candidates' sections cannot be retrieved rightly when circular dependencies exist. [#270](https://github.com/pdm-project/pdm/issues/270)\n- Don't pass the help argument into the run script method. [#272](https://github.com/pdm-project/pdm/issues/272)\n\n\nRelease v1.3.1 (2021-02-19)\n---------------------------\n\n### Bug Fixes\n\n- Use the absolute path when importing from a Poetry pyproject.toml. [#262](https://github.com/pdm-project/pdm/issues/262)\n- Fix a bug that old toml table head is kept when converting to PEP 621 metadata format. [#263](https://github.com/pdm-project/pdm/issues/263)\n- Postpone the evaluation of `requires-python` attribute when fetching the candidates of a package. [#264](https://github.com/pdm-project/pdm/issues/264)\n\n\nRelease v1.3.0 (2021-02-09)\n---------------------------\n\n### Features & Improvements\n\n- Increase the default value of the max rounds of resolution to 1000, make it configurable. [#238](https://github.com/pdm-project/pdm/issues/238)\n- Rewrite the project's `egg-info` directory when dependencies change. So that `pdm list --graph` won't show invalid entries. [#240](https://github.com/pdm-project/pdm/issues/240)\n- When importing requirements from a `requirements.txt` file, build the package to find the name if not given in the URL. [#245](https://github.com/pdm-project/pdm/issues/245)\n- When initializing the project, prompt user for whether the project is a library, and give empty `name` and `version` if not. [#253](https://github.com/pdm-project/pdm/issues/253)\n\n### Bug Fixes\n\n- Fix the version validator of wheel metadata to align with the implementation of `packaging`. [#130](https://github.com/pdm-project/pdm/issues/130)\n- Preserve the `sections` value of a pinned candidate to be reused. [#234](https://github.com/pdm-project/pdm/issues/234)\n- Strip spaces in user input when prompting for the python version to use. [#252](https://github.com/pdm-project/pdm/issues/252)\n- Fix the version parsing of Python requires to allow `>`, `>=`, `<`, `<=` to combine with star versions. [#254](https://github.com/pdm-project/pdm/issues/254)\n\n\nRelease v1.2.0 (2021-01-26)\n---------------------------\n\n### Features & Improvements\n\n- Change the behavior of `--save-compatible` slightly. Now the version specifier saved is using the REAL compatible operator `~=` as described in PEP 440. Before: `requests<3.0.0,>=2.19.1`, After: `requests~=2.19`. The new specifier accepts `requests==2.19.0` as compatible version. [#225](https://github.com/pdm-project/pdm/issues/225)\n- Environment variable `${PROJECT_ROOT}` in the dependency specification can be expanded to refer to the project root in pyproject.toml.\n  The environment variables will be kept as they are in the lock file. [#226](https://github.com/pdm-project/pdm/issues/226)\n- Change the dependencies of a package in the lock file to a list of PEP 508 strings [#236](https://github.com/pdm-project/pdm/issues/236)\n\n### Bug Fixes\n\n- Ignore user's site and `PYTHONPATH`(with `python -I` mode) when executing pip commands. [#231](https://github.com/pdm-project/pdm/issues/231)\n\n### Improved Documentation\n\n- Document about how to activate and use a plugin. [#227](https://github.com/pdm-project/pdm/issues/227)\n\n### Dependencies\n\n- Test project on `pip 21.0`. [#235](https://github.com/pdm-project/pdm/issues/235)\n\n\nRelease v1.1.0 (2021-01-18)\n---------------------------\n\n### Features & Improvements\n\n- Allow users to hide secrets from the `pyproject.toml`.\n  - Dynamically expand env variables in the URLs in dependencies and indexes.\n  - Ask whether to store the credentials provided by the user.\n  - A user-friendly error will show when credentials are not provided nor correct. [#198](https://github.com/pdm-project/pdm/issues/198)\n- Use a different package dir for 32-bit installation(Windows). [#212](https://github.com/pdm-project/pdm/issues/212)\n- Auto disable PEP 582 when a venv-like python is given as the interpreter path. [#219](https://github.com/pdm-project/pdm/issues/219)\n- Support specifying Python interpreter by `pdm use <path-to-python-root>`. [#221](https://github.com/pdm-project/pdm/issues/221)\n\n### Bug Fixes\n\n- Fix a bug of `PYTHONPATH` manipulation under Windows platform. [#215](https://github.com/pdm-project/pdm/issues/215)\n\n### Removals and Deprecations\n\n- Remove support of the old PEP 517 backend API path. [#217](https://github.com/pdm-project/pdm/issues/217)\n\n\nRelease v1.0.0 (2021-01-05)\n---------------------------\n\n### Bug Fixes\n\n- Correctly build wheels for dependencies with build-requirements but without a specified build-backend [#213](https://github.com/pdm-project/pdm/issues/213)\n\n\nRelease v1.0.0b2 (2020-12-29)\n-----------------------------\n\n### Features & Improvements\n\n- Fallback to pypi.org when `/search` endpoint is not available on given index. [#211](https://github.com/pdm-project/pdm/issues/211)\n\n### Bug Fixes\n\n- Fix a bug that PDM fails to parse python version specifiers with more than 3 parts. [#210](https://github.com/pdm-project/pdm/issues/210)\n\n\nRelease v1.0.0b0 (2020-12-24)\n-----------------------------\n\n### Features & Improvements\n\n- Fully support of PEP 621 specification.\n  - Old format is deprecated at the same time.\n  - PDM will migrate the project file for you when old format is detected.\n  - Other metadata formats(`Poetry`, `Pipfile`, `flit`) can also be imported as PEP 621 metadata. [#175](https://github.com/pdm-project/pdm/issues/175)\n- Re-implement the `pdm search` to query the `/search` HTTP endpoint. [#195](https://github.com/pdm-project/pdm/issues/195)\n- Reuse the cached built wheels to accelerate the installation. [#200](https://github.com/pdm-project/pdm/issues/200)\n- Make update strategy and save strategy configurable in pdm config. [#202](https://github.com/pdm-project/pdm/issues/202)\n- Improve the error message to give more insight on what to do when resolution fails. [#207](https://github.com/pdm-project/pdm/issues/207)\n- Set `classifiers` dynamic in `pyproject.toml` template for autogeneration. [#209](https://github.com/pdm-project/pdm/issues/209)\n\n### Bug Fixes\n\n- Fix a bug that distributions are not removed clearly in parallel mode. [#204](https://github.com/pdm-project/pdm/issues/204)\n- Fix a bug that python specifier `is_subset()` returns incorrect result. [#206](https://github.com/pdm-project/pdm/issues/206)\n\n\nRelease v0.12.3 (2020-12-21)\n----------------------------\n\n### Dependencies\n\n- Pin `pdm-pep517` to `<0.3.0`, this is the last version to support legacy project metadata format.\n\nRelease v0.12.2 (2020-12-17)\n----------------------------\n\n### Features & Improvements\n\n- Update the lock file schema, move the file hashes to `[metadata.files]` table. [#196](https://github.com/pdm-project/pdm/issues/196)\n- Retry failed jobs when syncing packages. [#197](https://github.com/pdm-project/pdm/issues/197)\n\n### Removals and Deprecations\n\n- Drop `pip-shims` package as a dependency. [#132](https://github.com/pdm-project/pdm/issues/132)\n\n### Miscellany\n\n- Fix the cache path for CI. [#199](https://github.com/pdm-project/pdm/issues/199)\n\n\nRelease v0.12.1 (2020-12-14)\n----------------------------\n\n### Features & Improvements\n\n- Provide an option to export requirements from pyproject.toml [#190](https://github.com/pdm-project/pdm/issues/190)\n- For Windows users, `pdm --pep582` can enable PEP 582 globally by manipulating the WinReg. [#191](https://github.com/pdm-project/pdm/issues/191)\n\n### Bug Fixes\n\n- Inject `__pypackages__` into `PATH` env var during `pdm run`. [#193](https://github.com/pdm-project/pdm/issues/193)\n\n\nRelease v0.12.0 (2020-12-08)\n----------------------------\n\n### Features & Improvements\n\n- Improve the user experience of `pdm run`:\n  - Add a special key in tool.pdm.scripts that holds configurations shared by all scripts.\n  - Support loading env var from a dot-env file.\n  - Add a flag `-s/--site-packages` to include system site-packages when running. [#178](https://github.com/pdm-project/pdm/issues/178)\n- Now PEP 582 can be enabled in the Python interpreter directly! [#181](https://github.com/pdm-project/pdm/issues/181)\n\n### Bug Fixes\n\n- Ensure `setuptools` is installed before invoking editable install script. [#174](https://github.com/pdm-project/pdm/issues/174)\n- Require `wheel` not `wheels` for global projects [#182](https://github.com/pdm-project/pdm/issues/182)\n- Write a `sitecustomize.py` instead of a `.pth` file to enable PEP 582. Thanks @Aloxaf.\n  Update `get_package_finder()` to be compatible with `pip 20.3`. [#185](https://github.com/pdm-project/pdm/issues/185)\n- Fix the help messages of commands \"cache\" and \"remove\" [#187](https://github.com/pdm-project/pdm/issues/187)\n\n\nRelease v0.11.0 (2020-11-20)\n----------------------------\n\n### Features & Improvements\n\n- Support custom script shortcuts in `pyproject.toml`.\n  - Support custom script shortcuts defined in `[tool.pdm.scripts]` section.\n  - Add `pdm run --list/-l` to show the list of script shortcuts. [#168](https://github.com/pdm-project/pdm/issues/168)\n- Patch the halo library to support parallel spinners.\n- Change the looking of `pdm install`. [#169](https://github.com/pdm-project/pdm/issues/169)\n\n### Bug Fixes\n\n- Fix a bug that package's marker fails to propagate to its grandchildren if they have already been resolved. [#170](https://github.com/pdm-project/pdm/issues/170)\n- Fix a bug that bare version specifiers in Poetry project can't be converted correctly. [#172](https://github.com/pdm-project/pdm/issues/172)\n- Fix the build error that destination directory is not created automatically. [#173](https://github.com/pdm-project/pdm/issues/173)\n\n\nRelease v0.10.2 (2020-11-05)\n----------------------------\n\n### Bug Fixes\n\n- Building editable distribution does not install `build-system.requires` anymore. [#167](https://github.com/pdm-project/pdm/issues/167)\n\n\nRelease v0.10.1 (2020-11-04)\n----------------------------\n\n### Bug Fixes\n\n- Switch the PEP 517 build frontend from `build` to a home-grown version. [#162](https://github.com/pdm-project/pdm/issues/162)\n- Synchronize the output of `LogWrapper`. [#164](https://github.com/pdm-project/pdm/issues/164)\n- Fix a bug that `is_subset` and `is_superset` may return wrong result when wildcard excludes overlaps with the upper bound. [#165](https://github.com/pdm-project/pdm/issues/165)\n\n\nRelease v0.10.0 (2020-10-20)\n----------------------------\n\n### Features & Improvements\n\n- Change to Git style config command. [#157](https://github.com/pdm-project/pdm/issues/157)\n- Add a command to generate scripts for autocompletion, which is backed by `pycomplete`. [#159](https://github.com/pdm-project/pdm/issues/159)\n\n### Bug Fixes\n\n- Fix a bug that `sitecustomize.py` incorrectly gets injected into the editable console scripts. [#158](https://github.com/pdm-project/pdm/issues/158)\n\n\nRelease v0.9.2 (2020-10-13)\n---------------------------\n\n### Features & Improvements\n\n- Cache the built wheels to accelerate resolution and installation process. [#153](https://github.com/pdm-project/pdm/issues/153)\n\n### Bug Fixes\n\n- Fix a bug that no wheel is matched when finding candidates to install. [#155](https://github.com/pdm-project/pdm/issues/155)\n- Fix a bug that installation in parallel will cause encoding initialization error on Ubuntu. [#156](https://github.com/pdm-project/pdm/issues/156)\n\n\nRelease v0.9.1 (2020-10-13)\n---------------------------\n\n### Features & Improvements\n\n- Display plain text instead of spinner bar under verbose mode. [#150](https://github.com/pdm-project/pdm/issues/150)\n\n### Bug Fixes\n\n- Fix a bug that the result of `find_matched()` is exhausted when accessed twice. [#149](https://github.com/pdm-project/pdm/issues/149)\n\n\nRelease v0.9.0 (2020-10-08)\n---------------------------\n\n### Features & Improvements\n\n- Allow users to combine several dependency sections to form an extra require. [#131](https://github.com/pdm-project/pdm/issues/131)\n- Split the PEP 517 backend to its own(battery included) package. [#134](https://github.com/pdm-project/pdm/issues/134)\n- Add a new option to list command to show reverse dependency graph. [#137](https://github.com/pdm-project/pdm/issues/137)\n\n### Bug Fixes\n\n- Fix a bug that spaces in path causes requirement parsing error. [#138](https://github.com/pdm-project/pdm/issues/138)\n- Fix a bug that requirement's python constraint is not respected when resolving. [#141](https://github.com/pdm-project/pdm/issues/141)\n\n### Dependencies\n\n- Update `pdm-pep517` to `0.2.0` that supports reading version from SCM. [#146](https://github.com/pdm-project/pdm/issues/146)\n\n### Miscellany\n\n- Add Python 3.9 to the CI version matrix to verify. [#144](https://github.com/pdm-project/pdm/issues/144)\n\n\nRelease v0.8.7 (2020-09-04)\n---------------------------\n\n### Bug Fixes\n\n- Fix a compatibility issue with `wheel==0.35`. [#135](https://github.com/pdm-project/pdm/issues/135)\n\n\nRelease v0.8.6 (2020-07-09)\n---------------------------\n\n### Bug Fixes\n\n- Fix a bug that extra sources are not respected when fetching distributions. [#127](https://github.com/pdm-project/pdm/issues/127)\n\n\nRelease v0.8.5 (2020-06-24)\n---------------------------\n\n### Bug Fixes\n\n- Fix a bug that `pdm export` fails when the project doesn't have `name` property. [#126](https://github.com/pdm-project/pdm/issues/126)\n\n### Dependencies\n\n- Upgrade dependency `pip` to `20.1`. [#125](https://github.com/pdm-project/pdm/issues/125)\n\n\nRelease v0.8.4 (2020-05-21)\n---------------------------\n\n### Features & Improvements\n\n- Add a new command `export` to export to alternative formats. [#117](https://github.com/pdm-project/pdm/issues/117)\n\n### Miscellany\n\n- Add Dockerfile and pushed to Docker Hub. [#122](https://github.com/pdm-project/pdm/issues/122)\n\n\nRelease v0.8.3 (2020-05-15)\n---------------------------\n\n### Bug Fixes\n\n- Fix the version constraint parsing of wheel metadata. [#120](https://github.com/pdm-project/pdm/issues/120)\n\n\nRelease v0.8.2 (2020-05-03)\n---------------------------\n\n### Bug Fixes\n\n- Update resolvers to `resolvelib` 0.4.0. [#118](https://github.com/pdm-project/pdm/issues/118)\n\n\nRelease v0.8.1 (2020-04-22)\n---------------------------\n\n### Dependencies\n\n- Switch to upstream `resolvelib 0.3.0`. [#116](https://github.com/pdm-project/pdm/issues/116)\n\n\nRelease v0.8.0 (2020-04-20)\n---------------------------\n\n### Features & Improvements\n\n- Add a new command to search for packages [#111](https://github.com/pdm-project/pdm/issues/111)\n- Add `show` command to show package metadata. [#114](https://github.com/pdm-project/pdm/issues/114)\n\n### Bug Fixes\n\n- Fix a bug that environment markers cannot be evaluated correctly if extras are connected with \"or\". [#107](https://github.com/pdm-project/pdm/issues/107)\n- Don't consult PyPI JSON API by default for package metadata. [#112](https://github.com/pdm-project/pdm/issues/112)\n- Eliminate backslashes in markers for TOML documents. [#115](https://github.com/pdm-project/pdm/issues/115)\n\n\nRelease v0.7.1 (2020-04-13)\n---------------------------\n\n### Bug Fixes\n\n- Editable packages requires `setuptools` to be installed in the isolated environment.\n\nRelease v0.7.0 (2020-04-12)\n---------------------------\n\n### Features & Improvements\n\n- Disable loading of site-packages under PEP 582 mode. [#100](https://github.com/pdm-project/pdm/issues/100)\n\n### Bug Fixes\n\n- Fix a bug that TOML parsing error is not correctly captured. [#101](https://github.com/pdm-project/pdm/issues/101)\n- Fix a bug of building wheels with C extensions that the platform in file name is incorrect. [#99](https://github.com/pdm-project/pdm/issues/99)\n\n\nRelease v0.6.5 (2020-04-07)\n---------------------------\n\n### Bug Fixes\n\n- Unix style executable script suffix is missing.\n\n\nRelease v0.6.4 (2020-04-07)\n---------------------------\n\n### Features & Improvements\n\n- Update shebang lines in the executable scripts when doing `pdm use`. [#96](https://github.com/pdm-project/pdm/issues/96)\n- Auto-detect commonly used venv directories. [#97](https://github.com/pdm-project/pdm/issues/97)\n\n\nRelease v0.6.3 (2020-03-30)\n---------------------------\n\n### Bug Fixes\n\n- Fix a bug of moving files across different file system. [#95](https://github.com/pdm-project/pdm/issues/95)\n\n\nRelease v0.6.2 (2020-03-29)\n---------------------------\n\n### Bug Fixes\n\n- Validate user input for `python_requires` when initializing project. [#89](https://github.com/pdm-project/pdm/issues/89)\n- Ensure `wheel` package is available before building packages. [#90](https://github.com/pdm-project/pdm/issues/90)\n- Fix an issue of remove command that will unexpectedly uninstall packages in default section. [#92](https://github.com/pdm-project/pdm/issues/92)\n\n### Dependencies\n\n- Update dependencies `pythonfinder`, `python-cfonts`, `pip-shims` and many others.\n  Drop dependency `vistir`. [#89](https://github.com/pdm-project/pdm/issues/89)\n\n\nRelease v0.6.1 (2020-03-25)\n---------------------------\n\n### Features & Improvements\n\n- Redirect output messages to log file for installation and locking. [#84](https://github.com/pdm-project/pdm/issues/84)\n\n### Bug Fixes\n\n- Fix a bug that parallel installation fails due to setuptools reinstalling. [#83](https://github.com/pdm-project/pdm/issues/83)\n\n\nRelease v0.6.0 (2020-03-20)\n---------------------------\n\n### Features & Improvements\n\n- Support specifying build script for C extensions. [#23](https://github.com/pdm-project/pdm/issues/23)\n- Add test cases for `pdm build`. [#81](https://github.com/pdm-project/pdm/issues/81)\n- Make it configurable whether to consult PyPI JSON API since it may be not trustable.\n- Support parallel installation.\n- Add new command `pmd import` to import project metadata from `Pipfile`, `poetry`, `flit`, `requirements.txt`.\n  [#79](https://github.com/pdm-project/pdm/issues/79)\n- `pdm init` and `pdm install` will auto-detect possible files that can be imported.\n\n### Bug Fixes\n\n- Fix wheel builds when `package_dir` is mapped. [#81](https://github.com/pdm-project/pdm/issues/81)\n- `pdm init` will use the current directory rather than finding the parents when\nglobal project is not activated.\n\n\nRelease v0.5.0 (2020-03-14)\n---------------------------\n\n### Features & Improvements\n\n- Introduce a super easy-to-extend plug-in system to PDM. [#75](https://github.com/pdm-project/pdm/issues/75)\n\n### Improved Documentation\n\n- Documentation on how to write a plugin. [#75](https://github.com/pdm-project/pdm/issues/75)\n\n### Bug Fixes\n\n- Fix a typo in metadata parsing from `plugins` to `entry_points`\n\n\nRelease v0.4.2 (2020-03-13)\n---------------------------\n\n### Features & Improvements\n\n- Refactor the CLI part, switch from `click` to `argparse`, for better extensibility. [#73](https://github.com/pdm-project/pdm/issues/73)\n- Allow users to configure to install packages into venv when it is activated. [#74](https://github.com/pdm-project/pdm/issues/74)\n\n\nRelease v0.4.1 (2020-03-11)\n---------------------------\n\n### Features & Improvements\n\n- Add a minimal dependency set for global project. [#72](https://github.com/pdm-project/pdm/issues/72)\n\n\nRelease v0.4.0 (2020-03-10)\n---------------------------\n\n### Features & Improvements\n\n- Global project support\n  - Add a new option `-g/--global` to manage global project. The default location is at `~/.pdm/global-project`.\n  - Use the virtualenv interpreter when detected inside an activated venv.\n  - Add a new option `-p/--project` to select project root other than the default one. [#30](https://github.com/pdm-project/pdm/issues/30)\n- Add a new command `pdm config del` to delete an existing config item. [#71](https://github.com/pdm-project/pdm/issues/71)\n\n### Bug Fixes\n\n- Fix a URL parsing issue that username will be dropped in the SSH URL. [#68](https://github.com/pdm-project/pdm/issues/68)\n\n### Improved Documentation\n\n- Add docs for global project and selecting project path. [#30](https://github.com/pdm-project/pdm/issues/30)\n\n\nRelease v0.3.2 (2020-03-08)\n---------------------------\n\n### Features & Improvements\n\n- Display all available Python interpreters if users don't give one in `pdm init`. [#67](https://github.com/pdm-project/pdm/issues/67)\n\n### Bug Fixes\n\n- Regard `4.0` as infinite upper bound when checking subsetting. [#66](https://github.com/pdm-project/pdm/issues/66)\n\n\nRelease v0.3.1 (2020-03-07)\n---------------------------\n\n### Bug Fixes\n\n- Fix a bug that `ImpossiblePySpec`'s hash clashes with normal one.\n\n\nRelease v0.3.0 (2020-02-28)\n---------------------------\n\n### Features & Improvements\n\n- Add a new command `pdm config` to inspect configurations. [#26](https://github.com/pdm-project/pdm/issues/26)\n- Add a new command `pdm cache clear` to clean caches. [#63](https://github.com/pdm-project/pdm/issues/63)\n\n### Bug Fixes\n\n- Correctly show dependency graph when circular dependencies exist. [#62](https://github.com/pdm-project/pdm/issues/62)\n\n### Improved Documentation\n\n- Write the initial documentation for PDM. [#14](https://github.com/pdm-project/pdm/issues/14)\n\n\nRelease v0.2.6 (2020-02-25)\n---------------------------\n\n### Features & Improvements\n\n- Improve the user interface of selecting Python interpreter. [#54](https://github.com/pdm-project/pdm/issues/54)\n\n### Bug Fixes\n\n- Fix the wheel installer to correctly unparse the flags of console scripts. [#56](https://github.com/pdm-project/pdm/issues/56)\n- Fix a bug that OS-dependent hashes are not saved. [#57](https://github.com/pdm-project/pdm/issues/57)\n\n\nRelease v0.2.5 (2020-02-22)\n---------------------------\n\n### Features & Improvements\n\n- Allow specifying Python interpreter via `--python` option in `pdm init`. [#49](https://github.com/pdm-project/pdm/issues/49)\n- Set `python_requires` when initializing and defaults to `>={current_version}`. [#50](https://github.com/pdm-project/pdm/issues/50)\n\n### Bug Fixes\n\n- Always consider wheels before tarballs; correctly merge markers from different parents. [#47](https://github.com/pdm-project/pdm/issues/47)\n- Filter out incompatible wheels when installing. [#48](https://github.com/pdm-project/pdm/issues/48)\n\n\nRelease v0.2.4 (2020-02-21)\n---------------------------\n\n### Bug Fixes\n\n- Use the project local interpreter to build wheels. [#43](https://github.com/pdm-project/pdm/issues/43)\n- Correctly merge Python specifiers when possible. [#4](https://github.com/pdm-project/pdm/issues/4)\n\n\nRelease v0.2.3 (2020-02-21)\n---------------------------\n\n### Bug Fixes\n\n- Fix a bug that editable build generates a malformed `setup.py`.\n\n\nRelease v0.2.2 (2020-02-20)\n---------------------------\n\n### Features & Improvements\n\n- Add a fancy greeting banner when user types `pdm --help`. [#42](https://github.com/pdm-project/pdm/issues/42)\n\n### Bug Fixes\n\n- Fix the RECORD file in built wheel. [#41](https://github.com/pdm-project/pdm/issues/41)\n\n### Dependencies\n\n- Add dependency `python-cfonts` to display banner. [#42](https://github.com/pdm-project/pdm/issues/42)\n\n\nRelease v0.2.1 (2020-02-18)\n---------------------------\n\n### Bug Fixes\n\n- Fix a bug that short python_version markers can't be parsed correctly. [#38](https://github.com/pdm-project/pdm/issues/38)\n- Make `_editable_install.py` compatible with Py2.\n\n\nRelease v0.2.0 (2020-02-14)\n---------------------------\n\n### Features & Improvements\n\n- New option: `pdm list --graph` to show a dependency graph of the working set. [#10](https://github.com/pdm-project/pdm/issues/10)\n- New option: `pdm update --unconstrained` to ignore the version constraint of given packages. [#13](https://github.com/pdm-project/pdm/issues/13)\n- Improve the error message when project is not initialized before running commands. [#19](https://github.com/pdm-project/pdm/issues/19)\n- Pinned candidates in lock file are reused when relocking during `pdm install`. [#33](https://github.com/pdm-project/pdm/issues/33)\n- Use the pyenv interpreter value if pyenv is installed. [#36](https://github.com/pdm-project/pdm/issues/36)\n- Introduce a new command `pdm info` to show project environment information. [#9](https://github.com/pdm-project/pdm/issues/9)\n\n### Bug Fixes\n\n- Fix a bug that candidate hashes will be lost when reused. [#11](https://github.com/pdm-project/pdm/issues/11)\n\n### Dependencies\n\n- Update `pip` to `20.0`, update `pip_shims` to `0.5.0`. [#28](https://github.com/pdm-project/pdm/issues/28)\n\n### Miscellany\n\n- Add a script named `setup_dev.py` for the convenience to setup pdm for development. [#29](https://github.com/pdm-project/pdm/issues/29)\n\n\nRelease v0.1.2 (2020-02-09)\n---------------------------\n\n### Features\n\n- New command pdm use to switch python versions. [#8](https://github.com/pdm-project/pdm/issues/8)\n- New option pdm list --graph to show a dependency graph. [#10](https://github.com/pdm-project/pdm/issues/10)\n- Read metadata from lockfile when pinned candidate is reused.\n\nRelease v0.1.1 (2020-02-07)\n---------------------------\n\n### Features\n\n- Get version from the specified file. [#6](https://github.com/pdm-project/pdm/issues/6)\n- Add column header to pdm list output.\n\nRelease v0.1.0 (2020-02-07)\n---------------------------\n\n### Bugfixes\n\n- Pass exit code to parent process in pdm run.\n- Fix error handling for CLI. [#19](https://github.com/pdm-project/pdm/issues/19)\n\n### Miscellany\n\n- Refactor the installer mocking for tests.\n\nRelease v0.0.5 (2020-01-22)\n---------------------------\n\n### Improvements\n\n- Ensure pypi index url is fetched in addition to the source settings. [#3](https://github.com/pdm-project/pdm/issues/3)\n\n### Bugfixes\n\n- Fix an issue that leading \"c\"s are mistakenly stripped. [#5](https://github.com/pdm-project/pdm/issues/5)\n- Fix an error with PEP 517 building.\n\nRelease v0.0.4 (2020-01-22)\n---------------------------\n\n### Improvements\n\n- Fix editable installation, now editable scripts can also be executed from outside!\n- Content hash is calculated based on dependencies and sources, not other metadata.\n\n### Bugfixes\n\n- Fix an issue that editable distributions can not be removed.\n\nRelease v0.0.3 (2020-01-22)\n---------------------------\n\n### Features\n\n- Add `pdm init` to bootstrap a project.\n\nRelease v0.0.2 (2020-01-22)\n---------------------------\n\n### Features\n\n- A complete functioning PEP 517 build backend.\n- `pdm build` command.\n\n### Miscellany\n\n- Add a Chinese README\n\n### Features\n\n- Add `pdm init` to bootstrap a project.\n\nRelease v0.0.1 (2020-01-20)\n---------------------------\n\n### Features\n\n- A dependency resolver that just works.\n- A PEP 582 installer.\n- PEP 440 version specifiers.\n- PEP 508 environment markers.\n- Running scripts with PEP 582 local packages.\n- Console scripts are injected with local paths.\n- A neat CLI.\n- add, lock, list, update, remove commands.\n- PEP 517 build backends.\n- Continuous Integration.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to PDM\n\nFirst off, thanks for taking the time to contribute! Contributions include but are not restricted to:\n\n- Reporting bugs\n- Contributing to code\n- Writing tests\n- Writing documentation\n\nThe following is a set of guidelines for contributing.\n\n## A recommended flow of contributing to an Open Source project\n\nThis section is for beginners to OSS. If you are an experienced OSS developer, you can skip this section.\n\n1. First, fork this project to your own namespace using the fork button at the top right of the repository page.\n\n2. Clone the **upstream** repository to local:\n\n    ```bash\n    git clone https://github.com/pdm-project/pdm.git\n    # Or if you prefer SSH clone:\n    git clone git@github.com:pdm-project/pdm.git\n    ```\n\n3. Add the fork as a new remote:\n\n    ```bash\n    git remote add fork https://github.com/yourname/pdm.git\n    git fetch fork\n    ```\n\n   Where `fork` is the remote name of the fork repository.\n\n**ProTips:**\n\n1. Don't modify code on the main branch, the main branch should always keep track of origin/main.\n\n    To update main branch to date:\n\n    ```bash\n    git pull origin main\n    # In rare cases that your local main branch diverges from the remote main:\n    git fetch origin && git reset --hard main\n    ```\n\n2. Create a new branch based on the up-to-date main branch for new patches.\n\n3. Create a Pull Request from that patch branch.\n\n## Local development\n\nWe recommend working in a virtual environment.\nFeel free to create a virtual environment with either the `venv` module or the `virtualenv` tool.\nFor example:\n\n```bash\npython -m venv .venv\n. .venv/bin/activate  # linux\n.venv/Scripts/activate  # windows\n```\n\nMake sure your `pip` is newer than `21.3` to install PDM in develop/editable mode:\n\n```bash\npython -m pip install -U \"pip>=21.3\"\npython -m pip install -e .\n```\n\nMake sure PDM uses the virtual environment you just created:\n\n```bash\npdm config -l python.use_venv true\npdm config -l venv.in_project true\n```\n\nInstall PDM development dependencies:\n\n```bash\npdm install\n```\n\nNow, all dependencies are installed into the Python environment you chose, which will be used for development after this point.\n\n### Run tests\n\n```bash\npdm run test\n```\n\nFaster test using pytest-xdist:\n\n```bash\npdm run test -n auto\n```\n\nThe test suite is still simple and needs expansion! Please help write more test cases.\n\n!!! note\n    You can also run your test suite against all supported Python version using `tox` with the `tox-pdm` plugin.\n    You can either run it by yourself with:\n\n    ```shell\n    tox\n    ```\n\n    Or from `pdm` with:\n\n    ```shell\n    pdm run tox\n    ```\n\n### Code style\n\nPDM uses pre-commit hooks for linting. You need to install [prek](https://github.com/j178/prek) to run the hooks.\n\nPlease refer to the [prek documentation](https://github.com/j178/prek?tab=readme-ov-file#installation) and install it with your preferred method.\n\nThen you can install the hooks by running:\n\n```bash\nprek install\n```\n\nYou can now lint the code with:\n\n```bash\npdm run lint\n```\n\nPDM uses `ruff` for code style and sorting import statements. If you are not following them,\nthe CI will fail and your Pull Request will not be merged.\n\n### News fragments\n\nWhen you make changes such as fixing a bug or adding a feature, you must add a news fragment describing your change.\n\nNews fragments are placed in the `news/` directory, and should be named according to this pattern: `<issue_num>.<issue_type>.md` (e.g., `566.bugfix.md`).\n\n#### Issue Types\n\n- `feature`: Features and improvements\n- `bugfix`: Bug fixes\n- `refactor`: Code restructures\n- `doc`: Added or improved documentation\n- `dep`: Changes to dependencies\n- `removal`: Removals or deprecations in the API\n- `misc`: Miscellaneous changes that don't fit any of the other categories\n\nThe contents of the file should be a single sentence in the imperative\nmood that describes your changes. (e.g. `Deduplicate the plugins list.` )\n\nSee entries in the [Change Log](/CHANGELOG.md) for more examples.\n\n### Preview the documentation\n\nPDM docs development requires a few additional dependencies. Install them as:\n\n```bash\nsudo apt install libffi-dev # Or equivalent with the package manager of your choice\n```\n\nNow, whenever you make some changes to the `docs/` and you want to preview the build result, simply do:\n\n```bash\npdm run doc\n```\n\n## Release\n\nOnce all changes are done and ready to release, you can preview the changelog contents by running:\n\n```bash\npdm run release --dry-run\n```\n\nMake sure the next version and the changelog are as expected in the output.\n\nThen cut a release on the **main** branch:\n\n```bash\npdm run release\n```\n\nGitHub action will create the release and upload the distributions to PyPI.\n\nRead more options about version bumping by `pdm run release --help`.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019-present Frost Ming\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\n# PDM\n\nA modern Python package and dependency manager supporting the latest PEP standards.\n[中文版本说明](README_zh.md)\n\n![PDM logo](https://raw.githubusercontent.com/pdm-project/pdm/main/docs/assets/logo_big.png)\n\n[![Docs](https://img.shields.io/badge/Docs-mkdocs-blue?style=for-the-badge)](https://pdm-project.org)\n[![Twitter Follow](https://img.shields.io/twitter/follow/pdm_project?label=get%20updates&logo=twitter&style=for-the-badge)](https://twitter.com/pdm_project)\n[![Discord](https://img.shields.io/discord/824472774965329931?label=discord&logo=discord&style=for-the-badge)](https://discord.gg/Phn8smztpv)\n\n![Github Actions](https://github.com/pdm-project/pdm/workflows/Tests/badge.svg)\n[![PyPI](https://img.shields.io/pypi/v/pdm?logo=python&logoColor=%23cccccc)](https://pypi.org/project/pdm)\n[![codecov](https://codecov.io/gh/pdm-project/pdm/branch/main/graph/badge.svg?token=erZTquL5n0)](https://codecov.io/gh/pdm-project/pdm)\n[![Packaging status](https://repology.org/badge/tiny-repos/pdm.svg)](https://repology.org/project/pdm/versions)\n[![Downloads](https://pepy.tech/badge/pdm/week)](https://pepy.tech/project/pdm)\n[![pdm-managed](https://img.shields.io/endpoint?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fpdm-project%2F.github%2Fbadge.json)](https://pdm-project.org)\n<a href=\"https://trackgit.com\">\n<img src=\"https://us-central1-trackgit-analytics.cloudfunctions.net/token/ping/l4eztudjnh9bfay668fl\" alt=\"trackgit-views\" />\n</a>\n\n[![asciicast](https://asciinema.org/a/jnifN30pjfXbO9We2KqOdXEhB.svg)](https://asciinema.org/a/jnifN30pjfXbO9We2KqOdXEhB)\n\n</div>\n\n## What is PDM?\n\nPDM is meant to be a next generation Python package management tool.\nIt was originally built for personal use. If you feel you are going well\nwith `Pipenv` or `Poetry` and don't want to introduce another package manager,\njust stick to it. But if you are missing something that is not present in those tools,\nyou can probably find some goodness in `pdm`.\n\n## Highlights of features\n\n- Simple and fast dependency resolver, mainly for large binary distributions.\n- A [PEP 517] build backend.\n- [PEP 621] project metadata.\n- Flexible and powerful plug-in system.\n- Versatile user scripts.\n- Install Pythons using [astral-sh's python-build-standalone](https://github.com/astral-sh/python-build-standalone).\n- Opt-in centralized installation cache like [pnpm](https://pnpm.io/motivation#saving-disk-space-and-boosting-installation-speed).\n\n[pep 517]: https://www.python.org/dev/peps/pep-0517\n[pep 621]: https://www.python.org/dev/peps/pep-0621\n[pnpm]: https://pnpm.io/motivation#saving-disk-space-and-boosting-installation-speed\n\n## Comparisons to other alternatives\n\n### [Pipenv](https://pipenv.pypa.io)\n\nPipenv is a dependency manager that combines `pip` and `venv`, as the name implies.\nIt can install packages from a non-standard `Pipfile.lock` or `Pipfile`.\nHowever, Pipenv does not handle any packages related to packaging your code,\nso it’s useful only for developing non-installable applications (Django sites, for example).\nIf you’re a library developer, you need `setuptools` anyway.\n\n### [Poetry](https://python-poetry.org)\n\nPoetry manages environments and dependencies in a similar way to Pipenv,\nbut it can also build .whl files with your code, and it can upload wheels and source distributions to PyPI.\nIt has a pretty user interface and users can customize it via a plugin. Poetry uses the `pyproject.toml` standard.\n\n### [Hatch](https://hatch.pypa.io)\n\nHatch can also manage environments, allowing multiple environments per project. By default it has a central location for all environments but it can be configured to put a project's environment(s) in the project root directory. It can manage packages but without lockfile support. It can also be used to package a project (with PEP 621 compliant pyproject.toml files) and upload it to PyPI.\n\n### This project\n\nPDM can manage virtual environments (venvs) in both project and centralized locations, similar to Pipenv. It reads project metadata from a standardized `pyproject.toml` file and supports lockfiles. Users can add additional functionality through plugins, which can be shared by uploading them as distributions.\n\nUnlike Poetry and Hatch, PDM is not limited to a specific build backend; users have the freedom to choose any build backend they prefer.\n\n## Installation\n\n<a href=\"https://repology.org/project/pdm/versions\">\n    <img src=\"https://repology.org/badge/vertical-allrepos/pdm.svg\" alt=\"Packaging status\" align=\"right\">\n</a>\n\nPDM requires python version 3.9 or higher. Alternatively, you can download the standalone binary file from the [release assets](https://github.com/pdm-project/pdm/releases).\n\n### Install Binary via Script (recommended)\n\nInstall the standalone binary directly with the installer scripts:\n\n**For Linux/Mac**\n\n```bash\ncurl -sSL https://pdm-project.org/install.sh | bash\n```\n\n**For Windows**\n\n```powershell\npowershell -ExecutionPolicy ByPass -c \"irm https://pdm-project.org/install.ps1 | iex\"\n```\n\nFor alternative installation methods (Python script, package managers, etc.), see the [installation section in documentation](https://pdm-project.org/en/latest/#installation).\n\n## Quickstart\n\n**Create a new PDM project**\n\n```bash\npdm new my-project\n```\n\nAnswer the questions following the guide, and a PDM project with a `pyproject.toml` file will be ready to use.\n\n**Install dependencies**\n\n```bash\npdm add requests flask\n```\n\nYou can add multiple dependencies in the same command. After a while, check the `pdm.lock` file to see what is locked for each package.\n\n## Badges\n\nTell people you are using PDM in your project by including the markdown code in README.md:\n\n```markdown\n[![pdm-managed](https://img.shields.io/endpoint?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fpdm-project%2F.github%2Fbadge.json)](https://pdm-project.org)\n```\n\n[![pdm-managed](https://img.shields.io/endpoint?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fpdm-project%2F.github%2Fbadge.json)](https://pdm-project.org)\n\n## PDM Eco-system\n\n[Awesome PDM](https://github.com/pdm-project/awesome-pdm) is a curated list of awesome PDM plugins and resources.\n\n## Experimental\n\nEnable [PEP 582](https://peps.python.org/pep-0582/) for a project:\n\n    pdm config python.use_venv False\n\nThis makes PDM install packages into a local project folder instead of a venv (similar to how npm installs into node_modules).\n    \nEnable [uv](https://github.com/astral-sh/uv) integration:\n\n    pdm config use_uv true\n\nuv is a very fast Python package installer written in Rust.\n    \nNote: `uv` does not work with `PEP 582`.\n\n## Sponsors\n\n<p align=\"center\">\n    <a href=\"https://cdn.jsdelivr.net/gh/pdm-project/sponsors/sponsors.svg\">\n        <img src=\"https://cdn.jsdelivr.net/gh/pdm-project/sponsors/sponsors.svg\"/>\n    </a>\n</p>\n\n## Credits\n\nThis project is strongly inspired by [pyflow] and [poetry].\n\n[pyflow]: https://github.com/David-OConnor/pyflow\n[poetry]: https://github.com/python-poetry/poetry\n\n## License\n\nThis project is open sourced under MIT license, see the [LICENSE](LICENSE) file for more details.\n"
  },
  {
    "path": "README_zh.md",
    "content": "<div align=\"center\">\n\n# PDM\n\n一个现代的 Python 包管理器，支持 PEP 最新标准。[English version README](README.md)\n\n![PDM logo](https://raw.githubusercontent.com/pdm-project/pdm/main/docs/assets/logo_big.png)\n\n[![Docs](https://img.shields.io/badge/Docs-mkdocs-blue?style=for-the-badge)](https://pdm-project.org)\n[![Twitter Follow](https://img.shields.io/twitter/follow/pdm_project?label=get%20updates&logo=twitter&style=for-the-badge)](https://twitter.com/pdm_project)\n[![Discord](https://img.shields.io/discord/824472774965329931?label=discord&logo=discord&style=for-the-badge)](https://discord.gg/Phn8smztpv)\n\n![Github Actions](https://github.com/pdm-project/pdm/workflows/Tests/badge.svg)\n[![PyPI](https://img.shields.io/pypi/v/pdm?logo=python&logoColor=%23cccccc)](https://pypi.org/project/pdm)\n[![Packaging status](https://repology.org/badge/tiny-repos/pdm.svg)](https://repology.org/project/pdm/versions)\n[![Downloads](https://pepy.tech/badge/pdm/week)](https://pepy.tech/project/pdm)\n[![pdm-managed](https://img.shields.io/endpoint?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fpdm-project%2F.github%2Fbadge.json)](https://pdm-project.org)\n\n[![asciicast](https://asciinema.org/a/jnifN30pjfXbO9We2KqOdXEhB.svg)](https://asciinema.org/a/jnifN30pjfXbO9We2KqOdXEhB)\n\n</div>\n\n## 这个项目是啥？\n\nPDM 旨在成为下一代 Python 软件包管理工具。它最初是为个人兴趣而诞生的。如果你觉得 `pipenv` 或者\n`poetry` 用着非常好，并不想引入一个新的包管理器，那么继续使用它们吧；但如果你发现有些东西这些\n工具不支持，那么你很可能可以在 `pdm` 中找到。\n\n## 主要特性\n\n- 一个简单且相对快速的依赖解析器，特别是对于大的二进制包发布。\n- 兼容 [PEP 517] 的构建后端，用于构建发布包 (源码格式与 wheel 格式)\n- 灵活且强大的插件系统\n- [PEP 621] 元数据格式\n- 功能强大的用户脚本\n- 支持从 [astral-sh's python-build-standalone](https://github.com/astral-sh/python-build-standalone) 安装 Python。\n- 像 [pnpm] 一样的中心化安装缓存，节省磁盘空间\n\n[pep 517]: https://www.python.org/dev/peps/pep-0517\n[pep 621]: https://www.python.org/dev/peps/pep-0621\n[pnpm]: https://pnpm.io/motivation#saving-disk-space-and-boosting-installation-speed\n\n## 与其他包管理器的比较\n\n### [Pipenv](https://pipenv.pypa.io)\n\nPipenv 是一个依赖管理器，它结合了 `pip` 和 `venv`，正如其名称所暗示的。它可以从一种自定义格式文件 `Pipfile.lock` 或 `Pipfile` 中安装软件包。\n然而，Pipenv 并不处理任何与构建、打包和发布相关的工作。所以它只适用于开发不可安装的应用程序（例如 Django 网站）。\n如果你是一个库的开发者，无论如何你都需要 `setuptools`。\n\n### [Poetry](https://python-poetry.org)\n\nPoetry 以类似于 Pipenv 的方式管理环境和依赖，它也可以从你的代码构建 `.whl` 文件，并且可以将轮子和源码发行版上传到 PyPI。\n它有一个漂亮的用户界面，用户可以通过贡献插件来定制它。Poetry 使用 `pyproject.toml` 标准。\n\n### [Hatch](https://hatch.pypa.io)\n\nHatch 也可以管理环境（它允许每个项目有多个环境，但不允许把它们放在项目目录中），并且可以管理包（但不支持 lockfile）。Hatch 也可以用来打包一个项目（用符合 PEP 621 标准的 `pyproject.toml` 文件）并上传到 PyPI。\n\n### 本项目\n\nPDM 也可以像 Pipenv 那样在项目或集中的位置管理 venvs。它从一个标准化的 `pyproject.toml` 文件中读取项目元数据，并支持 lockfile。用户可以在插件中添加更多的功能，并将其作为一个发行版上传，以供分享。\n\n此外，与 Poetry 和 Hatch 不同，PDM 并没有和任何特定的构建后端绑定，你可以选择任何你喜欢的构建后端。\n\n## 安装\n\n<a href=\"https://repology.org/project/pdm/versions\">\n    <img src=\"https://repology.org/badge/vertical-allrepos/pdm.svg\" alt=\"Packaging status\" align=\"right\">\n</a>\n\nPDM 需要 Python 3.9 或更高版本。你也可以从 [release assets](https://github.com/pdm-project/pdm/releases) 下载独立的可执行文件来使用。\n\n### 推荐：通过脚本安装二进制\n\n优先使用预构建的独立二进制，直接运行安装脚本即可：\n\n**Linux/Mac 安装命令**\n\n```bash\ncurl -sSL https://pdm-project.org/install.sh | bash\n```\n\n**Windows 安装命令**\n\n```powershell\npowershell -ExecutionPolicy ByPass -c \"irm https://pdm-project.org/install.ps1 | iex\"\n```\n\n其他安装方式（Python 安装脚本、包管理器等）请查看[安装文档](https://pdm-project.org/zh-cn/latest/#_3)。\n\n## 快速上手\n\n**初始化一个新的 PDM 项目**\n\n```bash\npdm new my_project\n```\n\n按照指引回答提示的问题，一个 PDM 项目和对应的`pyproject.toml`文件就创建好了。\n\n**添加依赖**\n\n```bash\npdm add requests flask\n```\n\n你可以在同一条命令中添加多个依赖。稍等片刻完成之后，你可以查看`pdm.lock`文件看看有哪些依赖以及对应版本。\n\n## 徽章\n\n在 README.md 中加入以下 Markdown 代码，向大家展示项目正在使用 PDM:\n\n```markdown\n[![pdm-managed](https://img.shields.io/endpoint?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fpdm-project%2F.github%2Fbadge.json)](https://pdm-project.org)\n```\n\n[![pdm-managed](https://img.shields.io/endpoint?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fpdm-project%2F.github%2Fbadge.json)](https://pdm-project.org)\n\n## PDM 生态\n\n[Awesome PDM](https://github.com/pdm-project/awesome-pdm) 这个项目收集了一些非常有用的 PDM 插件及相关资源。\n\n## 赞助\n\n<p align=\"center\">\n    <a href=\"https://cdn.jsdelivr.net/gh/pdm-project/sponsors/sponsors.svg\">\n        <img src=\"https://cdn.jsdelivr.net/gh/pdm-project/sponsors/sponsors.svg\"/>\n    </a>\n</p>\n\n## 鸣谢\n\n本项目的受到 [pyflow] 与 [poetry] 的很多启发。\n\n[pyflow]: https://github.com/David-OConnor/pyflow\n[poetry]: https://github.com/python-poetry/poetry\n\n## 使用许可\n\n本项目基于 MIT 协议开源，具体可查看 [LICENSE](./LICENSE)。\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nUse this section to tell people about which versions of your project are\ncurrently being supported with security updates.\n\n| Version | Supported          |\n| ------- | ------------------ |\n| Latest minor version   | :white_check_mark: |\n| Otherwise   | :x:                |\n\n## Reporting a Vulnerability\n\nIf you discover a potential security vulnerability, we kindly request that you refrain from sharing the information publicly and report it to us directly. \nPlease send an email to me@frostming.com with the following details:\n\n- Description of the potential vulnerability.\n- Steps to reproduce the issue (if applicable).\n- Any relevant screenshots or logs.\n- Your contact information for further communication.\n\nAlternatively, you can [open a security advisory](https://github.com/pdm-project/pdm/security/advisories/new) on GitHub.\n\n## Response Time\n\nUpon receiving your report, the maintainers will acknowledge receipt of your vulnerability report within 2 business days.\nWe will then review the reported issue and strive to keep you informed about our progress towards resolving it.\nYou can expect an update from us at least every 5 days until the issue is resolved.\n\n## Vulnerability Validation\n\nThe maintainers will assess the reported vulnerability and validate its existence. This process may involve a request for additional information from you.\nIf the vulnerability is confirmed, we will classify it based on its severity and potential impact.\n\nIf your reported vulnerability is validated and leads to a change in our systems, we will acknowledge your contribution in any public disclosure, unless you request anonymity.\nOtherwise, if the reported issue is not accepted as a vulnerability, we will provide a detailed explanation as to why we believe it does not pose a risk to our systems or users.\nWe value all reports and encourage you to continue to report any potential vulnerabilities you may find in the future.\n"
  },
  {
    "path": "codecov.yml",
    "content": "codecov:\n  notify:\n    after_n_builds: 12\n    wait_for_ci: false\n"
  },
  {
    "path": "docs/assets/extra.css",
    "content": "a.pdm-expansions {\n  cursor: pointer;\n  font-weight: bold;\n  color: currentColor;\n}\n\n.bot-container {\n  z-index: 9;\n  position: fixed;\n  width: 400px;\n  right: 20px;\n  bottom: 110px;\n  display: flex;\n  flex-direction: column;\n  align-items:end;\n}\n\n.bot-container > iframe {\n  width:100%;\n  border:none;\n  border-radius:0.5rem;\n  transition: height 0.3s ease-in-out;\n  height: 0;\n}\n.bot-button {\n  padding: 0.8rem;\n  border-radius: 50%;\n  width: 80px;\n  height: 80px;\n  background-color: var(--md-primary-fg-color);\n  transition: all 0.2s ease-in-out;\n  fill: white;\n}\n\n.bot-button:hover {\n  transform: translateY(-3px);\n  padding: 0.7rem;\n}\n\n/* for readthedocs badge */\n#readthedocs-embed-flyout {\n  position: sticky;\n  bottom: 0px;\n  width: auto;\n  max-width: 200px;\n}\n"
  },
  {
    "path": "docs/assets/extra.js",
    "content": "document.addEventListener('DOMContentLoaded', function () {\n  const expansionRepo = 'https://github.com/pdm-project/pdm-expansions';\n  const expansionsApi = 'https://expansion.pdm-project.org/api/sample';\n  const el = document.querySelector('a.pdm-expansions');\n\n  function loadExpansions() {\n    fetch(expansionsApi, { mode: 'cors', redirect: 'follow' })\n      .then((response) => {\n        console.log(response);\n        return response.json();\n      })\n      .then((data) => {\n        window.expansionList = data.data;\n        setExpansion();\n      });\n  }\n\n  function setExpansion() {\n    const { expansionList } = window;\n    if (!expansionList || !expansionList.length) {\n      window.location.href = expansionRepo;\n      return;\n    }\n    const expansion = expansionList[expansionList.length - 1];\n    expansionList.splice(expansionList.length - 1, 1);\n    el.innerText = expansion;\n    if (el.style.display == 'none') {\n      el.style.display = '';\n    }\n  }\n  loadExpansions();\n  el.addEventListener('click', function (e) {\n    e.preventDefault();\n    setExpansion();\n  });\n});\n"
  },
  {
    "path": "docs/dev/benchmark.md",
    "content": "# Benchmark\n\nThis page has been removed, please visit [Python Package Manager Shootout by Lincoln Loop](https://lincolnloop.github.io/python-package-manager-shootout/) for a detailed benchmark report.\n"
  },
  {
    "path": "docs/dev/changelog.md",
    "content": "# Changelog\n\n!!! warning \"Attention\"\n    Major and minor releases also include changes listed within prior beta releases.\n\n--8<-- \"CHANGELOG.md\"\n"
  },
  {
    "path": "docs/dev/contributing.md",
    "content": "--8<-- \"CONTRIBUTING.md\"\n"
  },
  {
    "path": "docs/dev/fixtures.md",
    "content": "# Pytest fixtures\n\n::: pdm.pytest\n    options:\n      show_source: false\n      show_root_heading: false\n      show_root_toc_entry: false\n      heading_level: 2\n"
  },
  {
    "path": "docs/dev/write.md",
    "content": "# PDM Plugins\n\nPDM is aiming at being a community driven package manager.\nIt is shipped with a full-featured plug-in system, with which you can:\n\n- Develop a new command for PDM\n- Add additional options to existing PDM commands\n- Change PDM's behavior by reading additional config items\n- Control the process of dependency resolution or installation\n\n## What should a plugin do\n\nThe core PDM project focuses on dependency management and package publishing.\nOther functionalities you wish to integrate with PDM are preferred to lie in their own plugins and released as standalone PyPI projects.\nIn case the plugin is considered a good supplement of the core project it may have a chance to be absorbed into PDM.\n\n## Write your own plugin\n\nIn the following sections, I will show an example of adding a new command `hello` which reads the `hello.name` config.\n\n### Write the command\n\nThe PDM's CLI module is designed in a way that user can easily \"inherit and modify\". To write a new command:\n\n```python\nfrom pdm.cli.commands.base import BaseCommand\n\nclass HelloCommand(BaseCommand):\n    \"\"\"Say hello to the specified person.\n    If none is given, will read from \"hello.name\" config.\n    \"\"\"\n\n    def add_arguments(self, parser):\n        parser.add_argument(\"-n\", \"--name\", help=\"the person's name to whom you greet\")\n\n    def handle(self, project, options):\n        if not options.name:\n            name = project.config[\"hello.name\"]\n        else:\n            name = options.name\n        print(f\"Hello, {name}\")\n```\n\nFirst, let's create a new `HelloCommand` class inheriting from `pdm.cli.commands.base.BaseCommand`. It has two major functions:\n\n- `add_arguments()` to manipulate the argument parser passed as the only argument, where you can add additional command line arguments to it\n- `handle()` to do something when the subcommand is matched, you can do nothing by writing a single `pass` statement. It accepts two arguments: an `pdm.project.Project` object as the first one and the parsed `argparse.Namespace` object as the second.\n\nThe document string will serve as the command help text, which will be shown in `pdm --help`.\n\nBesides, PDM's subcommand has two default options: `-v/--verbose` to change the verbosity level and `-g/--global` to enable global project.\nIf you don't want these default options, override the `arguments` class attribute to a list of `pdm.cli.options.Option` objects,\nor assign it to an empty list to have no default options:\n\n```python hl_lines=\"3\"\nclass HelloCommand(BaseCommand):\n\n    arguments = []\n```\n\n!!! note\n    The default options are loaded first, then `add_arguments()` is called.\n\n### Register the command to the core object\n\nWrite a function somewhere in your plugin project. There is no limit on what the name of the function is,\nbut the function should take only one argument -- the PDM core object:\n\n```python hl_lines=\"2\"\ndef hello_plugin(core):\n    core.register_command(HelloCommand, \"hello\")\n```\n\nCall `core.register_command()` to register the command. The second argument as the name of the subcommand is optional.\nPDM will look for the `HelloCommand`'s `name` attribute if the name is not passed.\n\n### Add a new config item\n\nLet's recall the first code snippet, `hello.name` config key is consulted for the name if not passed via the command line.\n\n```python hl_lines=\"11\"\nclass HelloCommand(BaseCommand):\n    \"\"\"Say hello to the specified person.\n    If none is given, will read from \"hello.name\" config.\n    \"\"\"\n\n    def add_arguments(self, parser):\n        parser.add_argument(\"-n\", \"--name\", help=\"the person's name to whom you greet\")\n\n    def handle(self, project, options):\n        if not options.name:\n            name = project.config[\"hello.name\"]\n        else:\n            name = options.name\n        print(f\"Hello, {name}\")\n```\n\nTill now, if you query the config value by `pdm config get hello.name`, an error will pop up saying it is not a valid config key.\nYou need to register the config item, too:\n\n```python hl_lines=\"5\"\nfrom pdm.project.config import ConfigItem\n\ndef hello_plugin(core):\n    core.register_command(HelloCommand, \"hello\")\n    core.add_config(\"hello.name\", ConfigItem(\"The person's name\", \"John\"))\n```\n\nwhere `ConfigItem` class takes 4 parameters, in the following order:\n\n- `description`: a description of the config item\n- `default`: default value of the config item\n- `global_only`: whether the config is allowed to set in home config only\n- `env_var`: the name of environment variable which will be read as the config value\n\n### Other plugin points\n\nBesides of commands and configurations, the `core` object exposes some other methods and attributes to override.\nPDM also provides some signals you can listen to.\nPlease read the [API reference](../reference/api.md) for more details.\n\n### Tips about developing a PDM plugin\n\nWhen developing a plugin, one hopes to activate and plugin in development and get updated when the code changes.\n\nYou can achieve this by installing the plugin in editable mode. To do this, specify the dependencies in `tool.pdm.plugins` array:\n\n```toml\n[tool.pdm]\nplugins = [\n    \"-e file:///${PROJECT_ROOT}\"\n]\n```\n\nThen install it with:\n\n```bash\npdm install --plugins\n```\n\nAfter that, all the dependencies are available in a project plugin library, including the plugin itself, in editable mode. That means any change\nto the codebase will take effect immediately without re-installation. The `pdm` executable also uses a Python interpreter under the hood,\nso if you run `pdm` from inside the plugin project, the plugin in development will be activated automatically, and you can do some testing to see how it works.\n\n### Testing your plugin\n\nPDM exposes some pytest fixtures as a plugin in the [`pdm.pytest`](fixtures.md) module.\nTo benefit from them, you must add `pdm[pytest]` as a test dependency.\n\nTo enable them in your test, add `pdm.pytest` as a plugin. You can do so by in your root `conftest.py`:\n\n```python title=\"conftest.py\"\n# single plugin\npytest_plugins = \"pytest.plugin\"\n\n# many plugins\npytest_plugins = [\n    ...\n    \"pdm.pytest\",\n    ...\n]\n```\n\nYou can see some usage examples into PDM own [tests](https://github.com/pdm-project/pdm/tree/main/tests), especially the [conftest.py file](https://github.com/pdm-project/pdm/blob/main/tests/conftest.py) for configuration.\n\nSee the [pytest fixtures documentation](fixtures.md) for more details.\n\n## Publish your plugin\n\nNow you have defined your plugin already, let's distribute it to PyPI. PDM's plugins are discovered by entry point types.\nCreate an `pdm` entry point and point to your plugin callable (yeah, it doesn't need to be a function, any callable object can work):\n\n**PEP 621**:\n\n```toml\n# pyproject.toml\n\n[project.entry-points.pdm]\nhello = \"my_plugin:hello_plugin\"\n```\n\n**setuptools**:\n\n```python\n# setup.py\n\nsetup(\n    ...\n    entry_points={\"pdm\": [\"hello = my_plugin:hello_plugin\"]}\n    ...\n)\n```\n\n## Activate the plugin\n\nAs plugins are loaded via entry points, they can be activated with no more steps than just installing the plugin.\nFor convenience, PDM provides a `plugin` command group to manage plugins.\n\nAssume your plugin is published as `pdm-hello`:\n\n```bash\npdm self add pdm-hello\n```\n\nNow type `pdm --help` in the terminal, you will see the new added `hello` command and use it:\n\n```bash\n$ pdm hello Jack\nHello, Jack\n```\n\nSee more plugin management subcommands by typing `pdm self --help` in the terminal.\n\n## Specify the plugins in project\n\nTo specify the required plugins for a project, you can use the `tool.pdm.plugins` config in the `pyproject.toml` file.\nThese dependencies can be installed into a project plugin library by running `pdm install --plugins`.\nThe project plugin library will be loaded in subsequent PDM commands.\n\nThis is useful when you want to share the same plugin set with the contributors.\n\n```toml\n# pyproject.toml\n[tool.pdm]\nplugins = [\n    \"pdm-packer\"\n]\n```\n\nRun `pdm install --plugins` to install and activate the plugins.\n\nAlternatively, you can have project-local plugins that are not published to PyPI, by using editable local dependencies:\n\n```toml\n# pyproject.toml\n[tool.pdm]\nplugins = [\n    \"-e file:///${PROJECT_ROOT}/plugins/my_plugin\"\n]\n```\n"
  },
  {
    "path": "docs/index.md",
    "content": "<div align=\"center\">\n<img src=\"assets/logo_big.png\" alt=\"PDM logo\">\n</div>\n\n# Introduction\n\nPDM, as described, is a modern Python package and dependency manager supporting the latest PEP standards. But it is more than a package manager. It boosts your development workflow in various aspects.\n\n<script id=\"asciicast-jnifN30pjfXbO9We2KqOdXEhB\" src=\"https://asciinema.org/a/jnifN30pjfXbO9We2KqOdXEhB.js\" async></script>\n\n## Feature highlights\n\n- Simple and fast dependency resolver, mainly for large binary distributions.\n- A [PEP 517] build backend.\n- [PEP 621] project metadata.\n- Flexible and powerful plug-in system.\n- Versatile user scripts.\n- Install Pythons using [indygreg's python-build-standalone](https://github.com/astral-sh/python-build-standalone).\n- Opt-in centralized installation cache like [pnpm].\n\n[pep 517]: https://www.python.org/dev/peps/pep-0517\n[pep 621]: https://www.python.org/dev/peps/pep-0621\n[pnpm]: https://pnpm.io/motivation#saving-disk-space-and-boosting-installation-speed\n\n## Installation\n\nPDM requires Python 3.9+ to be installed. It works on multiple platforms including Windows, Linux and macOS.\n\n!!! note\n    You can still have your project working on lower Python versions, read how to do it [here](usage/project.md#working-with-python-37).\n\n!!! note\n    Alternatively, you can download the standalone binary file from the [release assets](https://github.com/pdm-project/pdm/releases).\n\n### Recommended installation method\n\nInstall the prebuilt binary directly with the installer scripts.\n\n=== \"Linux/Mac\"\n\n    ```bash\n    curl -sSL https://pdm-project.org/install.sh | bash\n    ```\n\n    To install a specific version:\n\n    ```bash\n    curl -sSL https://pdm-project.org/install.sh | bash -s -- -v <version>\n    ```\n\n=== \"Windows\"\n\n    ```powershell\n    powershell -ExecutionPolicy ByPass -c \"irm https://pdm-project.org/install.ps1 | iex\"\n    ```\n\n    To install a specific version:\n\n    ```powershell\n    powershell -ExecutionPolicy ByPass -c \"irm https://pdm-project.org/install.ps1 | iex -Args '-v <version>'\"\n    ```\n\n### Install via Python script\n\nPDM requires python version 3.9 or higher.\n\nLike Pip, PDM provides an installation script that will install PDM into an isolated environment.\n\n=== \"Linux/Mac\"\n\n    ```bash\n    curl -sSL https://pdm-project.org/install-pdm.py | python3 -\n    ```\n\n=== \"Windows\"\n\n    ```powershell\n    powershell -ExecutionPolicy ByPass -c \"irm https://pdm-project.org/install-pdm.py | py -\"\n    ```\n\n!!! note\n    On Windows, if you do not have the optional `py` launcher installed (including if you installed Python through the Microsoft store), replace `py` with `python`.\n\nFor security reasons, you should verify the checksum of `install-pdm.py`.\nIt can be downloaded from [install-pdm.py.sha256](https://pdm-project.org/install-pdm.py.sha256).\n\nFor example, on Linux/Mac:\n\n```bash\ncurl -sSLO https://pdm-project.org/install-pdm.py\ncurl -sSL https://pdm-project.org/install-pdm.py.sha256 | shasum -a 256 -c -\n# Run the installer\npython3 install-pdm.py [options]\n```\n\nThe installer will install PDM into the user site and the location depends on the system:\n\n- `$HOME/.local/bin` for Unix\n- `$HOME/Library/Python/<version>/bin` for MacOS\n- `%APPDATA%\\Python\\Scripts` on Windows\n\nYou can pass additional options to the script to control how PDM is installed:\n\n```bash\nusage: install-pdm.py [-h] [-v VERSION] [--prerelease] [--remove] [-p PATH] [-d DEP]\n\noptional arguments:\n  -h, --help            show this help message and exit\n  -v VERSION, --version VERSION | envvar: PDM_VERSION\n                        Specify the version to be installed, or HEAD to install from the main branch\n  --prerelease | envvar: PDM_PRERELEASE    Allow prereleases to be installed\n  --remove | envvar: PDM_REMOVE            Remove the PDM installation\n  -p PATH, --path PATH | envvar: PDM_HOME  Specify the location to install PDM\n  -d DEP, --dep DEP | envvar: PDM_DEPS     Specify additional dependencies, can be given multiple times\n```\n\nYou can either pass the options after the script or set the env var value.\n\n### Other installation methods\n\n=== \"Homebrew\"\n\n    ```bash\n    brew install pdm\n    ```\n\n=== \"Scoop\"\n\n    ```\n    scoop bucket add frostming https://github.com/frostming/scoop-frostming.git\n    scoop install pdm\n    ```\n\n=== \"uv\"\n\n    ```bash\n    uv tool install pdm\n    ```\n\n=== \"pipx\"\n\n    ```bash\n    pipx install pdm\n    ```\n\n    Install the head version of GitHub repository.\n    Make sure you have installed [Git LFS](https://git-lfs.github.com/) on your system.\n\n    ```bash\n    pipx install git+https://github.com/pdm-project/pdm.git@main#egg=pdm\n    ```\n\n    To install PDM with all features:\n\n    ```bash\n    pipx install pdm[all]\n    ```\n\n    See also: <https://pypa.github.io/pipx/>\n\n=== \"pip\"\n\n    ```bash\n    pip install --user pdm\n    ```\n\n=== \"asdf\"\n\n    Assuming you have [asdf](https://asdf-vm.com/) installed.\n    ```\n    asdf plugin add pdm\n    asdf install pdm latest\n    asdf local pdm latest\n    ```\n\n=== \"inside project\"\n\n    By copying the [Pyprojectx](https://pyprojectx.github.io/) wrapper scripts to a project, you can install PDM as\n    (npm-style) dev dependency inside that project. This allows different projects/branches to use different PDM versions.\n\n    To [initialize a new or existing project](https://pyprojectx.github.io/usage/#initialize-a-new-or-existing-project),\n    cd into the project folder and:\n\n    === \"Linux/Mac\"\n\n        ```\n        curl -LO https://github.com/pyprojectx/pyprojectx/releases/latest/download/wrappers.zip && unzip wrappers.zip && rm -f wrappers.zip\n        ./pw --add pdm\n        ```\n\n    === \"Windows\"\n\n        ```powershell\n        Invoke-WebRequest https://github.com/pyprojectx/pyprojectx/releases/latest/download/wrappers.zip -OutFile wrappers.zip; Expand-Archive -Path wrappers.zip -DestinationPath .; Remove-Item -Path wrappers.zip\n        .\\pw --add pdm\n        ```\n\n    When installing pdm with this method, you need to run all `pdm` commands through the `pw` wrapper:\n\n    === \"Linux/Mac/Windows\"\n\n        ```\n        ./pw pdm install\n        ```\n\n### Update the PDM version\n\n```bash\npdm self update\n```\n\n### Uninstallation\n\nIf you need to remove PDM from your system, you can use the following script:\n\n=== \"Linux/Mac\"\n\n    ```bash\n    curl -sSL https://pdm-project.org/install-pdm.py | python3 - --remove\n    ```\n\n=== \"Windows\"\n\n    ```powershell\n    powershell -ExecutionPolicy ByPass -c \"irm https://pdm-project.org/install-pdm.py | py - --remove\"\n    ```\n\nIf you installed PDM using a third-party package management tool like Homebrew, you can also uninstall PDM using the tool's uninstall method, such as `brew uninstall pdm`.\n\n## Packaging Status\n\n[![Packaging status](https://repology.org/badge/vertical-allrepos/pdm.svg)](https://repology.org/project/pdm/versions)\n\n## Shell Completion\n\nPDM supports generating completion scripts for Bash, Zsh, Fish or Powershell. Here are some common locations for each shell:\n\n=== \"Bash\"\n\n    ```bash\n    pdm completion bash > /etc/bash_completion.d/pdm.bash-completion # Requires root (sudo). For an alternative, see next\n    pdm completion bash > ~/.bash_completion # Does not require root (sudo). Installed only for your user account\n    ```\n\n=== \"Zsh\"\n\n    ```bash\n    # Make sure ~/.zfunc is added to fpath, before compinit.\n    pdm completion zsh > ~/.zfunc/_pdm\n    ```\n\n    Oh-My-Zsh:\n\n    ```bash\n    mkdir $ZSH_CUSTOM/plugins/pdm\n    pdm completion zsh > $ZSH_CUSTOM/plugins/pdm/_pdm\n    ```\n\n    Then make sure pdm plugin is enabled in ~/.zshrc\n\n=== \"Fish\"\n\n    ```bash\n    pdm completion fish > ~/.config/fish/completions/pdm.fish\n    ```\n\n=== \"Powershell\"\n\n    ```ps1\n    # Create a directory to store completion scripts\n    mkdir $PROFILE\\..\\Completions\n    echo @'\n    Get-ChildItem \"$PROFILE\\..\\Completions\\\" | ForEach-Object {\n        . $_.FullName\n    }\n    '@ | Out-File -Append -Encoding utf8 $PROFILE\n    # Generate script\n    Set-ExecutionPolicy Unrestricted -Scope CurrentUser\n    pdm completion powershell | Out-File -Encoding utf8 $PROFILE\\..\\Completions\\pdm_completion.ps1\n    ```\n\n## Virtualenv and PEP 582\n\nPDM offers experimental support for [PEP 582](https://www.python.org/dev/peps/pep-0582/) as an opt-in feature, in addition to virtualenv management. Although [the Python Steering Council has rejected PEP 582][rejected], you can still test it out using PDM.\n\nTo learn more about the two modes, refer to the relevant chapters on [Working with virtualenv](usage/venv.md) and [Working with PEP 582](usage/pep582.md).\n\n[rejected]: https://discuss.python.org/t/pep-582-python-local-packages-directory/963/430\n\n## PDM Eco-system\n\n[Awesome PDM](https://github.com/pdm-project/awesome-pdm) is a curated list of awesome PDM plugins and resources.\n\n## Sponsors\n\n<p align=\"center\">\n    <a href=\"https://cdn.jsdelivr.net/gh/pdm-project/sponsors/sponsors.svg\">\n        <img src=\"https://cdn.jsdelivr.net/gh/pdm-project/sponsors/sponsors.svg\"/>\n    </a>\n</p>\n"
  },
  {
    "path": "docs/overrides/main.html",
    "content": "{% extends 'base.html' %}\n\n{% block announce %}\n<span class=\"twemoji\" style=\"vertical-align: middle;\">\n  {% include \".icons/octicons/heart-fill-24.svg\" %}\n</span>\n<a class=\"pdm-expansions\" style=\"display: none;\">Python Dependency Manager</a>\n{% endblock %}\n"
  },
  {
    "path": "docs/reference/api.md",
    "content": "# API Reference\n\n::: pdm.core.Core\n    options:\n      show_root_heading: yes\n      show_source: false\n      heading_level: 2\n\n::: pdm.core.Project\n    options:\n      show_root_heading: yes\n      show_source: false\n      heading_level: 2\n\n## Signals\n\n+++ 1.12.0\n\n::: pdm.signals\n    options:\n      heading_level: 3\n"
  },
  {
    "path": "docs/reference/build.md",
    "content": "# Build Configuration\n\n`pdm` uses the [PEP 517](https://www.python.org/dev/peps/pep-0517/) to build the package. It acts as a build frontend that calls the build backend to build the package.\n\nA build backend is what drives the build system to build source distributions and wheels from arbitrary source trees.\n\nIf you run [`pdm init`](../reference/cli.md#init), PDM will let you choose the build backend to use. Unlike other package managers, PDM does not force you to use a specific build backend. You can choose the one you like. Here is a list of build backends and corresponding configurations initially supported by PDM:\n\n=== \"pdm-backend\"\n\n    `pyproject.toml` configuration:\n\n    ```toml\n    [build-system]\n    requires = [\"pdm-backend\"]\n    build-backend = \"pdm.backend\"\n    ```\n\n    [:book: Read the docs](https://backend.pdm-project.org/)\n\n=== \"setuptools\"\n\n    `pyproject.toml` configuration:\n\n    ```toml\n    [build-system]\n    requires = [\"setuptools\", \"wheel\"]\n    build-backend = \"setuptools.build_meta\"\n    ```\n\n    [:book: Read the docs](https://setuptools.pypa.io/)\n\n=== \"flit\"\n\n    `pyproject.toml` configuration:\n\n    ```toml\n    [build-system]\n    requires = [\"flit_core >=3.2,<4\"]\n    build-backend = \"flit_core.buildapi\"\n    ```\n\n    [:book: Read the docs](https://flit.pypa.io/)\n\n=== \"hatchling\"\n\n    `pyproject.toml` configuration:\n\n    ```toml\n    [build-system]\n    requires = [\"hatchling\"]\n    build-backend = \"hatchling.build\"\n    ```\n\n    [:book: Read the docs](https://hatch.pypa.io/)\n\n=== \"maturin\"\n\n    `pyproject.toml` configuration:\n\n    ```toml\n    [build-system]\n    requires = [\"maturin>=1.4,<2.0\"]\n    build-backend = \"maturin\"\n    ```\n\n    [:book: Read the docs](https://www.maturin.rs/)\n\nApart from the above mentioned backends, you can also use any other backend that supports PEP 621, however, [poetry-core](https://python-poetry.org/) is not supported because it does not support reading PEP 621 metadata.\n\n!!! info\n    If you are using a custom build backend that is not in the above list, PDM will handle the relative paths as PDM-style(`${PROJECT_ROOT}` variable).\n"
  },
  {
    "path": "docs/reference/cli.md",
    "content": "# CLI Reference\n\n```python exec=\"1\" idprefix=\"\"\nimport argparse\nimport re\nfrom pdm.core import Core\n\nparser = Core().parser\n\nMONOSPACED = (\"pyproject.toml\", \"pdm.lock\", \".pdm-python\", \":pre\", \":post\", \":all\")\n\ndef clean_help(help: str) -> str:\n    # Make dunders monospaced avoiding italic markdown rendering\n    help = re.sub(r\"__([\\w\\d\\_]+)__\", r\"`__\\1__`\", help)\n    # Make env vars monospaced\n    help = re.sub(r\"env var: ([A-Z_]+)\", r\"env var: `\\1`\", help)\n    for monospaced in MONOSPACED:\n        help = re.sub(rf\"\\s(['\\\"]?{monospaced}['\\\"]?)\", f\"`{monospaced}`\", help)\n    return help\n\n\ndef render_parser(\n    parser: argparse.ArgumentParser, title: str, heading_level: int = 2\n) -> str:\n    \"\"\"Render the parser help documents as a string.\"\"\"\n    result = [f\"{'#' * heading_level} {title}\\n\"]\n    if parser.description and title != \"pdm\":\n        result.append(\"> \" + parser.description + \"\\n\")\n\n    for group in sorted(\n        parser._action_groups, key=lambda g: g.title.lower(), reverse=True\n    ):\n        if not any(\n            bool(action.option_strings or action.dest)\n            or isinstance(action, argparse._SubParsersAction)\n            for action in group._group_actions\n        ):\n            continue\n\n        result.append(f\"{group.title.title()}:\\n\")\n        for action in group._group_actions:\n            if isinstance(action, argparse._SubParsersAction):\n                for name, subparser in action._name_parser_map.items():\n                    result.append(render_parser(subparser, name, heading_level + 1))\n                continue\n\n            opts = [f\"`{opt}`\" for opt in action.option_strings]\n            if not opts:\n                line = f\"- `{action.dest}`\"\n            else:\n                line = f\"- {', '.join(opts)}\"\n            if action.metavar:\n                line += f\" `{action.metavar}`\"\n            line += f\": {clean_help(action.help)}\"\n            if action.default and action.default != argparse.SUPPRESS:\n                default = action.default\n                if any(opt.startswith(\"--no-\") for opt in action.option_strings) and default is True:\n                    default = not default\n                line += f\" (default: `{default}`)\"\n            result.append(line)\n        result.append(\"\")\n\n    return \"\\n\".join(result)\n\n\nprint(render_parser(parser, \"pdm\"))\n```\n"
  },
  {
    "path": "docs/reference/configuration.md",
    "content": "# Configurations\n\n[pdm-config]: ../reference/cli.md#config\n\n## Color Theme\n\nThe default theme used by PDM is as follows:\n\n| Key       | Default Style                                                |\n| --------- | ------------------------------------------------------------ |\n| `primary` | <span style=\"color:cyan\">cyan</span>                         |\n| `success` | <span style=\"color:green\">green</span>                       |\n| `warning` | <span style=\"color:yellow\">yellow</span>                     |\n| `error`   | <span style=\"color:red\">red</span>                           |\n| `info`    | <span style=\"color:blue\">blue</span>                         |\n| `req`     | <span style=\"color:green;font-weight:bold\">bold green</span> |\n\nYou can change the theme colors with [`pdm config`][pdm-config] command. For example, to change the `primary` color to `magenta`:\n\n```bash\npdm config theme.primary magenta\n```\n\nOr use a hex color code:\n\n```bash\npdm config theme.success '#51c7bd'\n```\n\n## Available Configurations\n\nThe following configuration items can be retrieved and modified by [`pdm config`][pdm-config] command.\n\n!!! note \"Environment Variable Overrides\"\n    If the corresponding env var is set, the value will take precedence over what is saved in the config file.\n\n```python exec=\"on\"\nfrom pdm.project.config import Config\n\nprint(\"| Config Item | Description | Default Value | Available in Project | Env var |\")\nprint(\"| --- | --- | --- | --- | --- |\")\nfor key, value in Config._config_map.items():\n    print(f\"| `{key}` | {value.description} | {('`%s`' % value.default) if value.should_show() else ''} | {'No' if value.global_only else 'Yes'} | {('`%s`' % value.env_var) if value.env_var else ''} |\")\nprint(\"\"\"\\\n| `pypi.<name>.url`                 | The URL of custom package source                                                     | `https://pypi.org/simple`                                             | Yes                  |                           |\n| `pypi.<name>.username`            | The username to access custom source                                                 |                                                                       | Yes                  |                           |\n| `pypi.<name>.password`            | The password to access custom source                                                 |                                                                       | Yes                  |                           |\n| `pypi.<name>.type`                | `index` or `find_links`                                                              | `index`                                                               | Yes                  |                           |\n| `pypi.<name>.verify_ssl`          | Verify SSL certificate when query custom source                                      | `True`                                                                | Yes                  |                           |\n| `repository.<name>.url`           | The URL of custom package source                                                     | `https://pypi.org/simple`                                             | Yes                  |                           |\n| `repository.<name>.username`      | The username to access custom repository                                             |                                                                       | Yes                  |                           |\n| `repository.<name>.password`      | The password to access custom repository                                             |                                                                       | Yes                  |                           |\n| `repository.<name>.ca_certs`      | Path to a PEM-encoded CA cert bundle (used for server cert verification)             | The CA certificates from [certifi](https://pypi.org/project/certifi/) | Yes                  |                           |\n| `repository.<name>.verify_ssl`    | Verify SSL certificate when uploading to repository                                  | `True`                                                                | Yes                  |                           |\n\"\"\")\n```\n"
  },
  {
    "path": "docs/reference/pep621.md",
    "content": "# PEP 621 Metadata\n\nThe project metadata are stored in the `pyproject.toml`. The specifications are defined by [PEP 621], [PEP 631] and [PEP 639]. Read the detailed specifications in the PEPs.\n\n[PEP 621]: https://www.python.org/dev/peps/pep-0621/\n[PEP 631]: https://www.python.org/dev/peps/pep-0631/\n[PEP 639]: https://www.python.org/dev/peps/pep-0639/\n\n_In the following part of this document, metadata should be written under `[project]` table if not given explicitly._\n\n## Multiline description\n\nYou can split a long description onto multiple lines, thanks to TOML support for multiline strings.\nJust remember to escape new lines, so the final description appears [on one line only in your package metadata](https://packaging.python.org/specifications/core-metadata/#summary).\nIndentation will be removed as well when escaping new lines:\n\n```toml\ndescription = \"\"\"\\\n    Lorem ipsum dolor sit amet, consectetur adipiscing elit, \\\n    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \\\n    Ut enim ad minim veniam, quis nostrud exercitation ullamco \\\n    laboris nisi ut aliquip ex ea commodo consequat.\\\n\"\"\"\n```\n\nSee [TOML's specification on strings](https://toml.io/en/v1.0.0#string).\n\n## Package version\n\n=== \"Static\"\n\n    ```toml\n    [project]\n    version = \"1.0.0\"\n    ```\n\n=== \"Dynamic\"\n\n    ```toml\n    [project]\n    ...\n    dynamic = [\"version\"]\n\n    [tool.pdm]\n    version = { source = \"file\", path = \"mypackage/__version__.py\" }\n    ```\n\n    The version will be read from the `mypackage/__version__.py` file searching for the pattern: `__version__ = \"{version}\"`.\n\n    Read more information about other configurations in [dynamic project version](https://backend.pdm-project.org/metadata/#dynamic-project-version) from the `pdm-backend` documentation.\n\n## Python version\n\nThe required version of Python is specified as the string `requires-python`:\n\n```toml\nrequires-python = \">=3.9\"\nclassifiers = [\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.9\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    ...\n]\n```\n\nNote: As per [PEP 621](https://peps.python.org/pep-0621/#allow-tools-to-add-extend-data),\nPDM is not permitted to dynamically update the `classifiers` section like some other non-compliant tools.\nThus, you should also include the appropriate [trove classifiers](https://pypi.org/classifiers/) as shown above if you plan on publishing your package on [PyPI](https://pypi.org/).\n\n## License\n\n<!-- TODO: update following paragraphs if PEP 639 is accepted,\nsee https://peps.python.org/pep-0639/#deprecate-license-classifiers -->\n\nThe license is specified as the string `license`:\n\n```toml\nlicense = {text = \"BSD-2-Clause\"}\nclassifiers = [\n    \"License :: OSI Approved :: BSD License\",\n    ...\n]\n```\n\nNote: As per [PEP 621](https://peps.python.org/pep-0621/#allow-tools-to-add-extend-data),\nPDM is not permitted to dynamically update the `classifiers` section like some other non-compliant tools.\nThus, you should also include the appropriate [trove classifiers](https://pypi.org/classifiers/) as shown above if you plan on publishing your package on [PyPI](https://pypi.org/).\n\n## Dependency specification\n\nThe `project.dependencies` is an array of dependency specification strings following the [PEP 440](https://www.python.org/dev/peps/pep-0440/) and [PEP 508](https://www.python.org/dev/peps/pep-0508/).\n\nExamples:\n\n```toml\n[project]\n...\ndependencies = [\n    # Named requirement\n    \"requests\",\n    # Named requirement with version specifier\n    \"flask >= 1.1.0\",\n    # Requirement with environment marker\n    \"pywin32; sys_platform == 'win32'\",\n    # URL requirement\n    \"pip @ git+https://github.com/pypa/pip.git@20.3.1\"\n]\n```\n\n## Optional dependencies\n\nYou can have some requirements optional, which is similar to `setuptools`' `extras_require` parameter.\n\n```toml\n[project.optional-dependencies]\nsocks = [ 'PySocks >= 1.5.6, != 1.5.7, < 2' ]\ntests = [\n  'ddt >= 1.2.2, < 2',\n  'pytest < 6',\n  'mock >= 1.0.1, < 4; python_version < \"3.4\"',\n]\n```\n\nTo install a group of optional dependencies:\n\n```bash\npdm install -G socks\n```\n\n`-G` option can be given multiple times to include more than one group.\n\n## Context variables expansion\n\nDepending on which build backend you are using, PDM will expand some variables in the dependency strings.\n\n### Environment variables\n\n=== \"pdm-backend\"\n\n    ```toml\n    [project]\n    dependencies = [\"flask @ https://${USERNAME}:${PASSWORD}/artifacts.io/Flask-1.1.2.tar.gz\"]\n    ```\n\n=== \"hatchling\"\n\n    ```toml\n    [project]\n    dependencies = [\"flask @ https://{env:USERNAME}:{env:PASSWORD}/artifacts.io/Flask-1.1.2.tar.gz\"]\n    ```\n\n    Find more usages [here](https://hatch.pypa.io/dev/config/context/#environment-variables)\n\nDon't worry about credential leakage, the environment variables will be expanded when needed and kept untouched in the lock file.\n\n### Relative paths\n\nWhen you add a package from a relative path, PDM will automatically save it as a relative path for `pdm-backend` and `hatchling`.\n\nFor example, if you run `pdm add ./my-package`, it will result in the following line in `pyproject.toml`.\n\n=== \"pdm-backend\"\n\n    ```toml\n    [project]\n    dependencies = [\"my-package @ file:///${PROJECT_ROOT}/my-package\"]\n    ```\n\n=== \"hatchling\"\n\n    ```toml\n    [project]\n    dependencies = [\"my-package @ {root:uri}/my-package\"]\n    ```\n\n    By default, hatchling doesn't support [direct references](https://hatch.pypa.io/dev/config/dependency/#direct-references)\n    in the dependency string, you need to turn it on in `pyproject.toml`:\n\n    ```toml\n    [tool.hatch.metadata]\n    allow-direct-references = true\n    ```\n\nThe relative path will be expanded based on the project root when installing or locking.\n\n## Console scripts\n\nThe following content:\n\n```toml\n[project.scripts]\nmycli = \"mycli.__main__:main\"\n```\n\nwill be translated to `setuptools` style:\n\n```python\nentry_points = {\n    'console_scripts': [\n        'mycli=mycli.__main__:main'\n    ]\n}\n```\n\nAlso, `[project.gui-scripts]` will be translated to `gui_scripts` entry points group in `setuptools` style.\n\n## Entry points\n\nOther types of entry points are given by `[project.entry-points.<type>]` section, with the same format of `[project.scripts]`:\n\n```toml\n[project.entry-points.pytest11]\nmyplugin = \"mypackage.plugin:pytest_plugin\"\n```\n\nIf the entry point name contains dots or other special characters, wrap it in quotes:\n\n```toml\n[project.entry-points.\"flake8.extension\"]\nmyplugin = \"mypackage.plugin:flake8_plugin\"\n```\n"
  },
  {
    "path": "docs/usage/advanced.md",
    "content": "# Advanced Usage\n\n## Automatic Testing\n\n### Use Tox as the runner\n\n[Tox](https://tox.readthedocs.io/en/latest/) is a great tool for testing against multiple Python versions or dependency sets.\nYou can configure a `tox.ini` like the following to integrate your testing with PDM:\n\n```ini\n[tox]\nenv_list = py{36,37,38},lint\n\n[testenv]\nsetenv =\n    PDM_IGNORE_SAVED_PYTHON=\"1\"\ndeps = pdm\ncommands =\n    pdm install --dev\n    pytest tests\n\n[testenv:lint]\ndeps = pdm\ncommands =\n    pdm install -G lint\n    flake8 src/\n```\n\nTo use the virtualenv created by Tox, you should make sure you have set `pdm config python.use_venv true`. PDM then will install\ndependencies from [`pdm lock`](../reference/cli.md#lock) into the virtualenv. In the dedicated venv you can directly run tools by `pytest tests/` instead\nof `pdm run pytest tests/`.\n\nYou should also make sure you don't run `pdm add/pdm remove/pdm update/pdm lock` in the test commands, otherwise the [`pdm lock`](../reference/cli.md#lock)\nfile will be modified unexpectedly. Additional dependencies can be supplied with the `deps` config. Besides, `isolated_build` and `passenv`\nconfig should be set as the above example to make PDM work properly.\n\nTo get rid of these constraints, there is a Tox plugin [tox-pdm](https://github.com/pdm-project/tox-pdm) which can ease the usage. You can install it by\n\n```bash\npip install tox-pdm\n```\n\nOr,\n\n```bash\npdm add --dev tox-pdm\n```\n\nAnd you can make the `tox.ini` much tidier as following, :\n\n```ini\n[tox]\nenv_list = py{36,37,38},lint\n\n[testenv]\ngroups = dev\ncommands =\n    pytest tests\n\n[testenv:lint]\ngroups = lint\ncommands =\n    flake8 src/\n```\n\nSee the [project's README](https://github.com/pdm-project/tox-pdm) for a detailed guidance.\n\n### Use Nox as the runner\n\n[Nox](https://nox.thea.codes/) is another great tool for automated testing. Unlike tox, Nox uses a standard Python file for configuration.\n\nIt is much easier to use PDM in Nox, here is an example of `noxfile.py`:\n\n```python hl_lines=\"4\"\nimport os\nimport nox\n\nos.environ.update({\"PDM_IGNORE_SAVED_PYTHON\": \"1\"})\n\n@nox.session\ndef tests(session):\n    session.run_always('pdm', 'install', '-G', 'test', external=True)\n    session.run('pytest')\n\n@nox.session\ndef lint(session):\n    session.run_always('pdm', 'install', '-G', 'lint', external=True)\n    session.run('flake8', '--import-order-style', 'google')\n```\n\nNote that `PDM_IGNORE_SAVED_PYTHON` should be set so that PDM can pick up the Python in the virtualenv correctly. Also make sure `pdm` is available in the `PATH`.\nBefore running nox, you should also ensure configuration item `python.use_venv` is true to enable venv reusing.\n\n### About PEP 582 `__pypackages__` directory\n\nBy default, if you run tools by [`pdm run`](../reference/cli.md#run), `__pypackages__` will be seen by the program and all subprocesses created by it. This means virtual environments created by those tools are also aware of the packages inside `__pypackages__`, which result in unexpected behavior in some cases.\nFor `nox`, you can avoid this by adding a line in `noxfile.py`:\n\n```python\nos.environ.pop(\"PYTHONPATH\", None)\n```\n\nFor `tox`, `PYTHONPATH` will not be passed to the test sessions so this isn't going to be a problem. Moreover, it is recommended to make `nox` and `tox` live in their own pipx environments so you don't need to install for every project. In this case, PEP 582 packages will not be a problem either.\n\n## Use PDM in Continuous Integration\n\nOnly one thing to keep in mind -- PDM can't be installed on Python < 3.7, so if your project is to be tested on those Python versions,\nyou have to make sure PDM is installed on the correct Python version, which can be different from the target Python version the particular job/task is run on.\n\nFortunately, if you are using GitHub Action, there is [pdm-project/setup-pdm](https://github.com/marketplace/actions/setup-pdm) to make this process easier.\nHere is an example workflow of GitHub Actions, while you can adapt it for other CI platforms.\n\n```yaml\nTesting:\n  runs-on: ${{ matrix.os }}\n  strategy:\n    matrix:\n      python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']\n      os: [ubuntu-latest, macOS-latest, windows-latest]\n\n  steps:\n    - uses: actions/checkout@v4\n    - name: Set up PDM\n      uses: pdm-project/setup-pdm@v4\n      with:\n        python-version: ${{ matrix.python-version }}\n\n    - name: Install dependencies\n      run: |\n        pdm sync -d -G testing\n    - name: Run Tests\n      run: |\n        pdm run -v pytest tests\n```\n\n!!! important \"TIPS\"\n    For GitHub Action users, there is a [known compatibility issue](https://github.com/actions/virtual-environments/issues/2803) on Ubuntu virtual environment.\n    If PDM parallel install is failed on that machine you should either set `parallel_install` to `false` or set env `LD_PRELOAD=/lib/x86_64-linux-gnu/libgcc_s.so.1`.\n    It is already handled by the `pdm-project/setup-pdm` action.\n\n!!! note\n    If your CI scripts run without a proper user set, you might get permission errors when PDM tries to create its cache directory.\n    To work around this, you can set the HOME environment variable yourself, to a writable directory, for example:\n\n    ```bash\n    export HOME=/tmp/home\n    ```\n\n## Use PDM in a multi-stage Dockerfile\n\nIt is possible to use PDM in a multi-stage Dockerfile to first install the project and dependencies into `__pypackages__`\nand then copy this folder into the final stage, adding it to `PYTHONPATH`.\n\n```dockerfile\nARG PYTHON_BASE=3.10-slim\n# build stage\nFROM python:$PYTHON_BASE AS builder\n\n# install PDM\nRUN pip install -U pdm\n# disable update check\nENV PDM_CHECK_UPDATE=false\n# copy files\nCOPY pyproject.toml pdm.lock README.md /project/\nCOPY src/ /project/src\n\n# install dependencies and project into the local packages directory\nWORKDIR /project\nRUN pdm install --check --prod --no-editable\n\n# run stage\nFROM python:$PYTHON_BASE\n\n# retrieve packages from build stage\nCOPY --from=builder /project/.venv/ /project/.venv\nENV PATH=\"/project/.venv/bin:$PATH\"\n# set command/entrypoint, adapt to fit your needs\nCOPY src /project/src\nCMD [\"python\", \"src/__main__.py\"]\n```\n\n## Use PDM to manage a monorepo\n\nWith PDM, you can have multiple sub-packages within a single project, each with its own `pyproject.toml` file. And you can create only one `pdm.lock` file to lock all dependencies. The sub-packages can have each other as their dependencies. To achieve this, follow these steps:\n\n`project/pyproject.toml`:\n\n```toml\n[dependency-groups]\ndev = [\n    \"-e file:///${PROJECT_ROOT}/packages/foo-core\",\n    \"-e file:///${PROJECT_ROOT}/packages/foo-cli\",\n    \"-e file:///${PROJECT_ROOT}/packages/foo-app\",\n]\n```\n\n`packages/foo-cli/pyproject.toml`:\n\n```toml\n[project]\ndependencies = [\"foo-core\"]\n```\n\n`packages/foo-app/pyproject.toml`:\n\n```toml\n[project]\ndependencies = [\"foo-core\"]\n```\n\nNow, run `pdm install` in the project root, and you will get a `pdm.lock` with all dependencies locked. All sub-packages will be installed in editable mode.\n\nLook at the [🚀 Example repository](https://github.com/pdm-project/pdm-example-monorepo) for more details.\n\n## Hooks for `pre-commit`\n\n[`pre-commit`](https://pre-commit.com/) is a powerful framework for managing git hooks in a centralized fashion. PDM already uses `pre-commit` [hooks](https://github.com/pdm-project/pdm/blob/main/.pre-commit-config.yaml) for its internal QA checks. PDM exposes also several hooks that can be run locally or in CI pipelines.\n\n### Export `requirements.txt`\n\nThis hook wraps the command `pdm export` along with any valid argument. It can be used as a hook (e.g., for CI) to ensure that you are going to check in the codebase a `requirements.txt`, which reflects the actual content of [`pdm lock`](../reference/cli.md#lock).\n\n```yaml\n# export python requirements\n- repo: https://github.com/pdm-project/pdm\n  rev: 2.x.y # a PDM release exposing the hook\n  hooks:\n    - id: pdm-export\n      # command arguments, e.g.:\n      args: ['-o', 'requirements.txt', '--without-hashes']\n      files: ^pdm.lock$\n```\n\n### Check `pdm.lock` is up to date with pyproject.toml\n\nThis hook wraps the command `pdm lock --check` along with any valid argument. It can be used as a hook (e.g., for CI) to ensure that whenever `pyproject.toml` has a dependency added/changed/removed, that `pdm.lock` is also up to date.\n\n```yaml\n- repo: https://github.com/pdm-project/pdm\n  rev: 2.x.y # a PDM release exposing the hook\n  hooks:\n    - id: pdm-lock-check\n```\n\n### Sync current working set with `pdm.lock`\n\nThis hook wraps the command `pdm sync` along with any valid argument. It can be used as a hook to ensure that your current working set is synced with `pdm.lock` whenever you checkout or merge a branch. Add _keyring_ to `additional_dependencies` if you want to use your systems credential store.\n\n```yaml\n- repo: https://github.com/pdm-project/pdm\n  rev: 2.x.y # a PDM release exposing the hook\n  hooks:\n    - id: pdm-sync\n      additional_dependencies:\n        - keyring\n```\n"
  },
  {
    "path": "docs/usage/config.md",
    "content": "# Configure the Project\n\nPDM's `config` command works just like `git config`, except that `--list` isn't needed to\nshow configurations.\n\nShow the current configurations:\n\n```bash\npdm config\n```\n\nGet one single configuration:\n\n```bash\npdm config pypi.url\n```\n\nChange a configuration value and store in home configuration:\n\n```bash\npdm config pypi.url \"https://test.pypi.org/simple\"\n```\n\nBy default, the configuration are changed globally, if you want to make the config seen by this project only, add a `--local` flag:\n\n```bash\npdm config --local pypi.url \"https://test.pypi.org/simple\"\n```\n\nAny local configurations will be stored in `pdm.toml` under the project root directory.\n\n## Configuration files\n\nThe configuration files are searched in the following order:\n\n1. `<PROJECT_ROOT>/pdm.toml` - The project configuration\n2. `<CONFIG_ROOT>/config.toml` - The home configuration\n3. `<SITE_CONFIG_ROOT>/config.toml` - The site configuration\n\nwhere `<CONFIG_ROOT>` is:\n\n- `$XDG_CONFIG_HOME/pdm` (`~/.config/pdm` in most cases) on Linux as defined by [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)\n- `~/Library/Application Support/pdm` on macOS as defined by [Apple File System Basics](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html)\n- `%USERPROFILE%\\AppData\\Local\\pdm\\pdm` on Windows as defined in [Known folders](https://docs.microsoft.com/en-us/windows/win32/shell/known-folders)\n\nand `<SITE_CONFIG_ROOT>` is:\n\n- `$XDG_CONFIG_DIRS/pdm` (`/etc/xdg/pdm` in most cases) on Linux as defined by [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)\n- `/Library/Application Support/pdm` on macOS as defined by [Apple File System Basics](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html)\n- `%ProgramData%\\pdm\\pdm` on Windows as defined in [Known folders](https://docs.microsoft.com/en-us/windows/win32/shell/known-folders)\n\nIf `-g/--global` option is used, the first item will be replaced by `<CONFIG_ROOT>/global-project/pdm.toml`.\n\nYou can find all available configuration items in [Configuration Page](../reference/configuration.md).\n\n## Configure the Python finder\n\nBy default, PDM will try to find Python interpreters in the following sources:\n\n- `venv`: The PDM virtualenv location\n- `path`: The `PATH` environment variable\n- `pyenv`: The [pyenv](https://github.com/pyenv/pyenv) install root\n- `rye`: The [rye](https://rye-up.com/) toolchain install root\n- `asdf`: The [asdf](https://asdf-vm.com/) python install root\n- `winreg`: The Windows registry\n\nYou can unselect some of them or change the order by setting `python.providers` config key:\n\n```bash\npdm config python.providers rye   # Rye source only\npdm config python.providers pyenv,asdf  # pyenv and asdf\n```\n\n## Allow prereleases in resolution result\n\nBy default, `pdm`'s dependency resolver will ignore prereleases unless there are no stable versions for the given version range of a dependency. This behavior can be changed by setting `allow-prereleases` to `true` in `[tool.pdm.resolution]` table:\n\n```toml\n[tool.pdm.resolution]\nallow-prereleases = true\n```\n\n## Configure the package indexes\n\nYou can tell PDM where to to find the packages by either specifying sources in the `pyproject.toml` or via `pypi.*` configurations.\n\nAdd sources in `pyproject.toml`:\n\n```toml\n[[tool.pdm.source]]\nname = \"private\"\nurl = \"https://private.pypi.org/simple\"\nverify_ssl = true\n```\n\nChange the default index via `pdm config`:\n\n```bash\npdm config pypi.url \"https://test.pypi.org/simple\"\n```\n\nAdd extra indexes via `pdm config`:\n\n```bash\npdm config pypi.extra.url \"https://extra.pypi.org/simple\"\n```\n\nThe available configuration options are:\n\n- `url`: The URL of the index\n- `verify_ssl`: (Optional)Whether to verify SSL certificates, default to true\n- `username`: (Optional)The username for the index\n- `password`: (Optional)The password for the index\n- `type`: (Optional) index or find_links, default to index\n\n??? note \"About the source types\"\n    By default, all sources are [PEP 503](https://www.python.org/dev/peps/pep-0503/) style \"indexes\" like pip's `--index-url` and `--extra-index-url`, however, you can set the type to `find_links` which contains files or links to be looked for directly. See [this answer](https://stackoverflow.com/a/46651848) for the difference between the two types.\n\n    For example, to use a local directory as a source:\n\n    ```toml\n    [[tool.pdm.source]]\n    name = \"local\"\n    url = \"file:///${PROJECT_ROOT}/packages\"\n    type = \"find_links\"\n    ```\n\nThese configurations are read in the following order to build the final source list:\n\n- `pypi.url`, if `pypi` doesn't appear in the `name` field of any source in `pyproject.toml`\n- Sources in `pyproject.toml`\n- `pypi.<name>.url` in PDM config.\n\nYou can set `pypi.ignore_stored_index` to `true` to disable all additional indexes from the PDM config and only use those specified in `pyproject.toml`.\n\n!!! tip \"Disable the default PyPI index\"\n    If you want to omit the default PyPI index, just set the source name to `pypi` and that source will **replace** it.\n\n    ```toml\n    [[tool.pdm.source]]\n    url = \"https://private.pypi.org/simple\"\n    verify_ssl = true\n    name = \"pypi\"\n    ```\n\n??? note \"Indexes in `pyproject.toml` or config\"\n    When you want to share the indexes with other people who are going to use the project, you should add them in `pyproject.toml`.\n    For example, some packages only exist in a private index and can't be installed if someone doesn't configure the index.\n    Otherwise, store them in the local config which won't be seen by others.\n\n### Respect the order of the sources\n\nBy default, all sources are considered equal, packages from them are sorted by the version and wheel tags, the most matching one with the highest version is selected.\n\nIn some cases you may want to return packages from the preferred source, and search for others if they are missing from the former source. PDM supports this by reading the configuration `respect-source-order`. For example:\n\n```toml\n[tool.pdm.resolution]\nrespect-source-order = true\n\n[[tool.pdm.source]]\nname = \"private\"\nurl = \"https://private.pypi.org/simple\"\n\n[[tool.pdm.source]]\nname = \"pypi\"\nurl = \"https://pypi.org/simple\"\n```\n\nA package will be searched from the `private` index first, and only if no matching version is found there, it will be searched from the `pypi` index.\n\n### Specify index for individual packages\n\nYou can bind packages to specific sources with `include_packages` and `exclude_packages` config under `tool.pdm.source` table.\n\n```toml\n[[tool.pdm.source]]\nname = \"private\"\nurl = \"https://private.pypi.org/simple\"\ninclude_packages = [\"foo\", \"foo-*\"]\nexclude_packages = [\"bar-*\"]\n```\n\nWith the above configuration, any package matching `foo` or `foo-*` will only be searched from the `private` index, and any package matching `bar-*` will be searched from all indexes except `private`.\n\nBoth `include_packages` and `exclude_packages` are optional and accept a list of glob patterns, and `include_packages` takes effect exclusively when the pattern matches.\n\n### Store credentials with the index\n\nYou can specify credentials in the URL with `${ENV_VAR}` variable expansion and these variables will be read from the environment variables:\n\n```toml\n[[tool.pdm.source]]\nname = \"private\"\nurl = \"https://${PRIVATE_PYPI_USERNAME}:${PRIVATE_PYPI_PASSWORD}@private.pypi.org/simple\"\n```\n\n### Configure HTTPS certificates\n\nYou can use a custom CA bundle or client certificate for HTTPS requests. It can be configured for both indexes(for package download) and repositories(for upload):\n\n```bash\npdm config pypi.ca_certs /path/to/ca_bundle.pem\npdm config repository.pypi.ca_certs /path/to/ca_bundle.pem\n```\n\nBesides, it is possible to use the system trust store, instead of the bundled certifi certificates for verifying HTTPS certificates. This approach will typically support corporate proxy certificates without additional configuration.\n\nTo use `truststore`, you need Python 3.10 or newer and install `truststore` into the same environment as PDM:\n\n```bash\npdm self add truststore\n```\n\nIn addition, CA certificates specified by env vars `REQUESTS_CA_BUNDLE` and `CURL_CA_BUNDLE` will be used if they are set.\n\n### Index configuration merging\n\nIndex configurations are merged with the `name` field of `[[tool.pdm.source]]` table or `pypi.<name>` key in the config file.\nThis enables you to store the url and credentials separately, to avoid secrets being exposed in the source control.\nFor example, if you have the following configuration:\n\n```toml\n[[tool.pdm.source]]\nname = \"private\"\nurl = \"https://private.pypi.org/simple\"\n```\n\nYou can store the credentials in the config file:\n\n```bash\npdm config pypi.private.username \"foo\"\npdm config pypi.private.password \"bar\"\n```\n\nPDM can retrieve the configurations for `private` index from both places.\n\nIf the index requires a username and password, but they can't be found from the environment variables nor config file, PDM will prompt you to enter them. Or, if `keyring` is installed, it will be used as the credential store. PDM can use the `keyring` from either the installed package or the CLI.\n\n## Central installation caches\n\nIf a package is required by many projects on the system, each project has to keep its own copy. This can be a waste of disk space, especially for data science and machine learning projects.\n\nPDM supports _caching_ installations of the same wheel by installing it in a centralized package repository and linking to that installation in different projects. To enable it, run:\n\n```bash\npdm config install.cache on\n```\n\nIt can be enabled on a per-project basis by adding the `--local` option to the command.\n\nThe caches are located in `$(pdm config cache_dir)/packages`. You can view the cache usage with `pdm cache info`. Note that the cached installations are managed automatically -- they will be deleted if they are not linked to any projects. Manually deleting the caches from disk may break some projects on the system.\n\nIn addition, several different link methods are supported:\n\n- `symlink`(default), create symlinks to the package files.\n- `hardlink`, create hard links to the package files of the cache entry.\n\nYou can switch between them by running `pdm config [--local] install.cache_method <method>`.\n\n!!! note\n    Only packages installed from one of the package sources can be cached.\n\n## Configure the repositories for upload\n\nWhen using the [`pdm publish`](../reference/cli.md#publish) command, it reads the repository secrets from the **global** config file(`<CONFIG_ROOT>/config.toml`). The content of the config is as follows:\n\n```toml\n[repository.pypi]\nusername = \"frostming\"\npassword = \"<secret>\"\n\n[repository.company]\nurl = \"https://pypi.company.org/legacy/\"\nusername = \"frostming\"\npassword = \"<secret>\"\nca_certs = \"/path/to/custom-cacerts.pem\"\n```\n\nAlternatively, these credentials can be provided with env vars:\n\n```bash\nexport PDM_PUBLISH_REPO=...\nexport PDM_PUBLISH_USERNAME=...\nexport PDM_PUBLISH_PASSWORD=...\nexport PDM_PUBLISH_CA_CERTS=...\n```\n\nA PEM-encoded Certificate Authority bundle (`ca_certs`) can be used for local / custom PyPI repositories where the server certificate is not signed by the standard [certifi](https://github.com/certifi/python-certifi/blob/master/certifi/cacert.pem) CA bundle.\n\n!!! note\n    Repositories are different from indexes in the previous section. Repositories are for publishing while indexes are for locking\n    and resolving. They don't share the configuration.\n\n!!! tip\n    You don't need to configure the `url` for `pypi` and `testpypi` repositories, they are filled by default values.\n    The username, password, and certificate authority bundle can be passed in from the command line for `pdm publish` via `--username`, `--password`, and `--ca-certs`, respectively.\n\nTo change the repository config from the command line, use the [`pdm config`](../reference/cli.md#config) command:\n\n```bash\npdm config repository.pypi.username \"__token__\"\npdm config repository.pypi.password \"my-pypi-token\"\n\npdm config repository.company.url \"https://pypi.company.org/legacy/\"\npdm config repository.company.ca_certs \"/path/to/custom-cacerts.pem\"\n```\n\n## Password management with keyring\n\nWhen keyring is available and supported, the passwords will be stored to and retrieved from the keyring instead of writing to the config file. This supports both indexes and upload repositories. The service name will be `pdm-pypi-<name>` for an index and `pdm-repository-<name>` for a repository.\n\nYou can enable keyring by either installing `keyring` into the same environment as PDM or installing globally. To add keyring to the PDM environment:\n\n```bash\npdm self add keyring\n```\n\nAlternatively, if you have installed a copy of keyring globally, make sure the CLI is exposed in the `PATH` env var to make it discoverable by PDM:\n\n```bash\nexport PATH=$PATH:path/to/keyring/bin\n```\n\n### Password management with keyring for Azure Artifacts\n\nWhen trying to authenticate towards azure artifacts, this can be achieved by either using AD groups to authenticate: `pdm self add keyring artifacts-keyring` ensuring that artifacts-keyring will be used for authentication.\n\nAnd then adding the artifacts url to `pyproject.toml`\n\n```toml\n[[tool.pdm.source]]\nname = \"NameOfFeed\"\nurl = \"https://pkgs.dev.azure.com/[org name]/_packaging/[feed name]/pypi/simple/\"\n```\n\n## Exclude specific packages and their dependencies from the lock file\n\n+++ 2.12.0\n\nSometimes you don't even want to include certain packages in the locked file because you are sure they won't be used by any code. In this case, you can completely skip them and their dependencies during dependency resolution:\n\n```toml\n[tool.pdm.resolution]\nexcludes = [\"requests\"]\n```\n\nWith this config, `requests` will not be locked in the lockfile, and its dependencies such as `urllib3` and `idna` will also not show up in the resolution result, if not depended on by other packages. The installer will not be able to pick them up either.\n\n## Passing constant arguments to every pdm invocation\n\n+++ 2.7.0\n\nYou can add extra options passed to individual pdm commands by `tool.pdm.options` configuration:\n\n```toml\n[tool.pdm.options]\nadd = [\"--no-isolation\", \"--no-self\"]\ninstall = [\"--no-self\"]\nlock = [\"--no-cross-platform\"]\n```\n\nThese options will be added right after the command name. For instance, based on the configuration above,\n`pdm add requests` is equivalent to `pdm add --no-isolation --no-self requests`.\n\n## Ignore package warnings\n\n+++ 2.10.0\n\nYou may see some warnings when resolving dependencies like this:\n\n```bash\nPackageWarning: Skipping scipy@1.10.0 because it requires Python\n<3.12,>=3.8 but the project claims to work with Python>=3.9.\nNarrow down the `requires-python` range to include this version. For example, \">=3.9,<3.12\" should work.\n  warnings.warn(record.message, PackageWarning, stacklevel=1)\nUse `-q/--quiet` to suppress these warnings, or ignore them per-package with `ignore_package_warnings` config in [tool.pdm] table.\n```\n\nThis is because the supported range of Python versions of the package doesn't cover the `requires-python` value specified in the `pyproject.toml`.\nYou can ignore these warnings in a per-package basis by adding the following config:\n\n```toml\n[tool.pdm]\nignore_package_warnings = [\"scipy\", \"tensorflow-*\"]\n```\n\nWhere each item is a case-insensitive glob pattern to match the package name.\n"
  },
  {
    "path": "docs/usage/dependency.md",
    "content": "# Manage Dependencies\n\nPDM provides a bunch of useful commands to help manage your project and dependencies.\nThe following examples are run on Ubuntu 18.04, a few changes must be done if you are using Windows.\n\n## Add dependencies\n\n[`pdm add`](../reference/cli.md#add) can be followed by one or several dependencies, and the dependency specification is described in [PEP 508](https://www.python.org/dev/peps/pep-0508/).\n\nExamples:\n\n```bash\npdm add requests   # add requests\npdm add requests==2.25.1   # add requests with version constraint\npdm add requests[socks]   # add requests with extra dependency\npdm add \"flask>=1.0\" flask-sqlalchemy   # add multiple dependencies with different specifiers\n```\n\nPDM also allows extra dependency groups by providing `-G/--group <name>` option, and those dependencies will go to\n`[project.optional-dependencies.<name>]` table in the project file, respectively.\n\nYou can reference other optional groups in `optional-dependencies`, even before the package is uploaded:\n\n```toml\n[project]\nname = \"foo\"\nversion = \"0.1.0\"\n\n[project.optional-dependencies]\nsocks = [\"pysocks\"]\njwt = [\"pyjwt\"]\nall = [\"foo[socks,jwt]\"]\n```\n\nAfter that, dependencies and sub-dependencies will be resolved properly and installed for you, you can view `pdm.lock` to see the resolved result of all dependencies.\n\n### Local dependencies\n\nLocal packages can be added with their paths. The path can be a file or a directory:\n\n```bash\npdm add ./sub-package\npdm add ./first-1.0.0-py2.py3-none-any.whl\n```\n\nThe paths MUST start with a `.`, otherwise it will be recognized as a normal named requirement. The local dependencies will be written to the `pyproject.toml` file with the URL format:\n\n```toml\n[project]\ndependencies = [\n    \"sub-package @ file:///${PROJECT_ROOT}/sub-package\",\n    \"first @ file:///${PROJECT_ROOT}/first-1.0.0-py2.py3-none-any.whl\",\n]\n```\n\n??? note \"Using other build backends\"\n    If you are using `hatchling` instead of the pdm backend, the URLs would be as follows:\n\n    ```\n    sub-package @ {root:uri}/sub-package\n    first @ {root:uri}/first-1.0.0-py2.py3-none-any.whl\n    ```\n    Other backends doesn't support encoding relative paths in the URL and will write the absolute path instead.\n\n### URL dependencies\n\nPDM also supports downloading and installing packages directly from a web address.\n\nExamples:\n\n```bash\n# Install gzipped package from a plain URL\npdm add \"https://github.com/numpy/numpy/releases/download/v1.20.0/numpy-1.20.0.tar.gz\"\n# Install wheel from a plain URL\npdm add \"https://github.com/explosion/spacy-models/releases/download/en_core_web_trf-3.5.0/en_core_web_trf-3.5.0-py3-none-any.whl\"\n```\n\n### VCS dependencies\n\nYou can also install from a git repository url or other version control systems. The following are supported:\n\n- Git: `git`\n- Mercurial: `hg`\n- Subversion: `svn`\n- Bazaar: `bzr`\n\nThe URL should be like: `{vcs}+{url}@{rev}`\n\nExamples:\n\n```bash\n# Install pip repo on tag `22.0`\npdm add \"git+https://github.com/pypa/pip.git@22.0\"\n# Provide credentials in the URL\npdm add \"git+https://username:password@github.com/username/private-repo.git@master\"\n# Give a name to the dependency\npdm add \"pip @ git+https://github.com/pypa/pip.git@22.0\"\n# Or use the #egg fragment\npdm add \"git+https://github.com/pypa/pip.git@22.0#egg=pip\"\n# Install from a subdirectory\npdm add \"git+https://github.com/owner/repo.git@master#egg=pkg&subdirectory=subpackage\"\n```\n\nTo use ssh scheme for git, just replace `https://` to `ssh://git@`\n\nExample:\n\n```bash\npdm add \"wheel @ git+ssh://git@github.com/pypa/wheel.git@main\"\n```\n\nOr the short non-URI form, which uses a colon(`:`) to separate the host and path:\n\n```bash\npdm add \"wheel @ git+git@github.com:pypa/wheel.git@main\"\n```\n\n### Hide credentials in the URL\n\nYou can hide the credentials in the URL by using the `${ENV_VAR}` variable syntax:\n\n```toml\n[project]\ndependencies = [\n  \"mypackage @ git+http://${VCS_USER}:${VCS_PASSWD}@test.git.com/test/mypackage.git@master\"\n]\n```\n\nThese variables will be read from the environment variables when installing the project.\n\n### Add development only dependencies\n\n+++ 1.5.0\n\nPDM also supports defining groups of dependencies that are useful for development,\ne.g. some for testing and others for linting. We usually don't want these dependencies to appear in the distribution's metadata\nso using `optional-dependencies` is probably not a good idea. We can define them as development dependencies:\n\n```bash\npdm add -dG test pytest\n```\n\nThis will result in a `pyproject.toml` as following:\n\n```toml\n[dependency-groups]\ntest = [\"pytest\"]\n```\n\nYou can have several groups of development only dependencies. Unlike `optional-dependencies`, they won't appear in the package distribution metadata such as `PKG-INFO` or `METADATA`,\nwhich means the package index won't be aware of these dependencies. The schema is similar to that of `optional-dependencies`.\n\n```toml\n[dependency-groups]\nlint = [\n    \"flake8\",\n    \"black\"\n]\ntest = [\"pytest\", \"pytest-cov\"]\ndoc = [\"mkdocs\"]\n```\n\nFor backward-compatibility, if only `-d` or `--dev` is specified, dependencies will go to `dev` group under `[dependency-groups]` by default.\n\n!!! note\n    The same group name MUST NOT appear in both `[dependency-groups]` and `[project.optional-dependencies]`.\n\n### Editable dependencies\n\n**Local directories** and **VCS dependencies** can be installed in [editable mode](https://pip.pypa.io/en/stable/cli/pip_install/#editable-installs). If you are familiar with `pip`, it is just like `pip install -e <package>`. **Editable packages are allowed only in development dependencies**:\n\n!!! note\n    Editable installs are only allowed in the `dev` dependency group. Other groups, including the default, will fail with a `[PdmUsageError]`.\n\n```bash\n# A relative path to the directory\npdm add -e ./sub-package --dev\n# A file URL to a local directory\npdm add -e file:///path/to/sub-package --dev\n# A VCS URL\npdm add -e git+https://github.com/pallets/click.git@main#egg=click --dev\n```\n\n### Save version specifiers\n\nIf the package is given without a version specifier like `pdm add requests`.\nPDM provides three different behaviors of what version specifier is saved for the dependency,\nwhich is given by `--save-<strategy>`(Assume `2.21.0` is the latest version that can be found for the dependency):\n\n- `minimum`: Save the minimum version specifier: `>=2.21.0` (default).\n- `compatible`: Save the compatible version specifier: `>=2.21.0,<3.0.0`.\n- `exact`: Save the exact version specifier: `==2.21.0`.\n- `wildcard`: Don't constrain version and leave the specifier to be wildcard: `*`.\n\n### Add prereleases\n\nOne can give `--pre/--prerelease` option to [`pdm add`](../reference/cli.md#add) so that prereleases are allowed to be pinned for the given packages.\n\n## Update existing dependencies\n\nTo update all dependencies in the lock file:\n\n```bash\npdm update\n```\n\nTo update the specified package(s):\n\n```bash\npdm update requests\n```\n\nTo update multiple groups of dependencies:\n\n```bash\npdm update -G security -G http\n```\n\nOr using comma-separated list:\n\n```bash\npdm update -G \"security,http\"\n```\n\nTo update a given package in the specified group:\n\n```bash\npdm update -G security cryptography\n```\n\nIf the group is not given, PDM will search for the requirement in the default dependencies set and raises an error if none is found.\n\nTo update packages in development dependencies:\n\n```bash\n# Update all default + dev-dependencies\npdm update -d\n# Update a package in the specified group of dev-dependencies\npdm update -dG test pytest\n```\n\n### About update strategy\n\nSimilarly, PDM also provides 3 different behaviors of updating dependencies and sub-dependencies,\nwhich is given by `--update-<strategy>` option:\n\n- `reuse`: Keep all locked dependencies except for those given in the command line (default).\n- `reuse-installed`: Try to reuse the versions installed in the working set. **This will also affect the packages requested in the command line**.\n- `eager`: Try to lock a newer version of the packages in command line and their recursive sub-dependencies and keep other dependencies as they are.\n- `all`: Update all dependencies and sub-dependencies.\n\n### Update packages to the versions that break the version specifiers\n\nOne can give `-u/--unconstrained` to tell PDM to ignore the version specifiers in the `pyproject.toml`.\nThis works similarly to the `yarn upgrade -L/--latest` command. Besides,\n[`pdm update`](../reference/cli.md#update_2) also supports the `--pre/--prerelease` option.\n\n## Remove existing dependencies\n\nTo remove existing dependencies from project file and the library directory:\n\n```bash\n# Remove requests from the default dependencies\npdm remove requests\n# Remove h11 from the 'web' group of optional-dependencies\npdm remove -G web h11\n# Remove pytest-cov from the `test` group of dependency-groups\npdm remove -dG test pytest-cov\n```\n\n## List outdated packages and the latest versions\n\n+++ 2.13.0\n\nTo list outdated packages and the latest versions:\n\n```bash\npdm outdated\n```\n\nYou can pass glob patterns to filter the packages to show:\n\n```bash\npdm outdated requests* flask*\n```\n\n## Select a subset of dependency groups to install\n\nSay we have a project with following dependencies:\n\n```toml\n[project]  # This is production dependencies\ndependencies = [\"requests\"]\n\n[project.optional-dependencies]  # This is optional dependencies\nextra1 = [\"flask\"]\nextra2 = [\"django\"]\n\n[dependency-groups]  # This is dev dependencies\ndev1 = [\"pytest\"]\ndev2 = [\"mkdocs\"]\n```\n\n| Command                         | What it does                                                         | Comments                  |\n| ------------------------------- | -------------------------------------------------------------------- | ------------------------- |\n| `pdm install`                   | install all groups locked in the lockfile                            |                           |\n| `pdm install -G extra1`         | install prod deps, dev deps, and \"extra1\" optional group             |                           |\n| `pdm install -G dev1`           | install prod deps and only \"dev1\" dev group                          |                           |\n| `pdm install -G:all`            | install prod deps, dev deps and \"extra1\", \"extra2\" optional groups   |                           |\n| `pdm install -G extra1 -G dev1` | install prod deps, \"extra1\" optional group and only \"dev1\" dev group |                           |\n| `pdm install --prod`            | install prod only                                                    |                           |\n| `pdm install --prod -G extra1`  | install prod deps and \"extra1\" optional                              |                           |\n| `pdm install --prod -G dev1`    | Fail, `--prod` can't be given with dev dependencies                  | Leave the `--prod` option |\n\n**All** development dependencies are included as long as `--prod` is not passed and `-G` doesn't specify any dev groups.\n\nBesides, if you don't want the root project to be installed, add `--no-self` option, and `--no-editable` can be used when you want all packages to be installed in non-editable versions.\n\nYou may also use the pdm lock command with these options to lock only the specified groups, which will be recorded in the `[metadata]` table of the lock file. If no `--group/--prod/--dev/--no-default` option is specified, `pdm sync` and `pdm update` will operate using the groups in the lockfile. However, if any groups that are not included in the lockfile are given as arguments to the commands, PDM will raise an error.\n\n## Dependency Overrides\n\nIf none of versions of a specific package doesn't meet all the constraints, the resolution will fail. In this case, you can tell the resolver to use a specific version of the package with dependency overrides.\n\nOverrides are a useful last resort for cases in which the user knows that a dependency is compatible with a newer version of a package than the package declares, but the package has not yet been updated to declare that compatibility.\n\nFor example, if a transitive dependency declares `pydantic>=1.0,<2.0`, but the user knows that the package is compatible with `pydantic>=2.0`, the user can override the declared dependency with `pydantic>=2.0,<3` to allow the resolver to continue.\n\nIn PDM, there are two ways to specify overrides:\n\n### In the project file\n\n+++ 1.12.0\n\nYou can specify the overrides in the `pyproject.toml` file, under the `[tool.pdm.resolution.overrides]` table:\n\n```toml\n[tool.pdm.resolution.overrides]\nasgiref = \"3.2.10\"  # exact version\nurllib3 = \">=1.26.2\"  # version range\npytz = \"https://mypypi.org/packages/pytz-2020.9-py3-none-any.whl\"  # absolute URL\n```\n\nEach entry in the table is a package name and a version specifier. The version specifier can be a version range, an exact version, or an absolute URL.\n\n### Via CLI option\n\n+++ 2.17.0\n\nPDM also supports reading dependency overrides from a requirements file. The file works similarly to the constraint file in pip(`--constraint constraints.txt`), and the syntax is the same as the requirements file:\n\n```\nrequests==2.20.0\ndjango==1.11.8\ncertifi==2018.11.17\nchardet==3.0.4\nidna==2.7\npytz==2019.3\nurllib3==1.23\n```\n\nOverride files serve as an easy way to store the dependencies in a centralized location that can be shared by multiple projects in your organization.\n\nYou can pass the constraint file to various PDM commands that would perform a resolution, such as [`pdm install`](../reference/cli.md#install), [`pdm lock`](../reference/cli.md#lock), [`pdm add`](../reference/cli.md#add), etc.\n\n```bash\npdm lock --override constraints.txt\n```\n\nThis option can be supplied multiple times.\n\nOverride files can also be served via a URL, e.g. `--override http://example.com/constraints.txt`, so that your organization can store and serve them in a remote server.\n\n## Show what packages are installed\n\nSimilar to `pip list`, you can list all packages installed in the packages directory:\n\n```bash\npdm list\n```\n\n### Include and exclude groups\n\nBy default, all packages installed in the working set will be listed. You can specify which groups to be listed\nby `--include/--exclude` options, and `include` has a higher priority than `exclude`.\n\n```bash\npdm list --include dev\npdm list --exclude test\n```\n\nThere is a special group `:sub`, when included, all transitive dependencies will also be shown. It is included by default.\n\nYou can also pass `--resolve` to `pdm list`, which will show the packages resolved in `pdm.lock`, rather than installed in the working set.\n\n### Change the output fields and format\n\nBy default, name, version and location will be shown in the list output, you can view more fields or specify the order of fields by `--fields` option:\n\n```bash\npdm list --fields name,licenses,version\n```\n\nFor all supported fields, please refer to the [CLI reference](../reference/cli.md#list_1).\n\nAlso, you can specify the output format other than the default table output. The supported formats and options are `--csv`, `--json`, `--markdown` and `--freeze`.\n\n### Show the dependency tree\n\nOr show a dependency tree by:\n\n```bash\n$ pdm list --tree\ntempenv 0.0.0\n└── click 7.0 [ required: <7.0.0,>=6.7 ]\nblack 19.10b0\n├── appdirs 1.4.3 [ required: Any ]\n├── attrs 19.3.0 [ required: >=18.1.0 ]\n├── click 7.0 [ required: >=6.5 ]\n├── pathspec 0.7.0 [ required: <1,>=0.6 ]\n├── regex 2020.2.20 [ required: Any ]\n├── toml 0.10.0 [ required: >=0.9.4 ]\n└── typed-ast 1.4.1 [ required: >=1.4.0 ]\nbump2version 1.0.0\n```\n\nNote that `--fields` option doesn't work with `--tree`.\n\n### Filter packages by patterns\n\nYou can also limit the packages to show by passing the patterns to `pdm list`:\n\n```bash\npdm list flask-* requests-*\n```\n\n??? warning \"Be careful with the shell expansion\"\n    In most shells, the wildcard `*` will be expanded if there are matching files under the current directory.\n    To avoid getting unexpected results, you can wrap the patterns with single quotes: `pdm list 'flask-*' 'requests-*'`.\n\nIn `--tree` mode, only the subtree of the matched packages will be displayed. This can be used to achieve the same purpose as `pnpm why`, which is to show why a specific package is required.\n\n```bash\n$ pdm list --tree --reverse certifi\ncertifi 2023.7.22\n└── requests 2.31.0 [ requires: >=2017.4.17 ]\n    └── cachecontrol[filecache] 0.13.1 [ requires: >=2.16.0 ]\n```\n\n## Manage global project\n\nSometimes users may want to keep track of the dependencies of global Python interpreter as well.\nIt is easy to do so with PDM, via `-g/--global` option which is supported by most subcommands.\n\nIf the option is passed, `<CONFIG_ROOT>/global-project` will be used as the project directory, which is\nalmost the same as normal project except that `pyproject.toml` will be created automatically for you\nand it doesn't support build features. The idea is taken from Haskell's [stack](https://docs.haskellstack.org).\n\nHowever, unlike `stack`, by default, PDM won't use global project automatically if a local project is not found.\nUsers should pass `-g/--global` explicitly to activate it, since it is not very pleasing if packages go to a wrong place.\nBut PDM also leave the decision to users, just set the config `global_project.fallback` to `true`.\n\nBy default, when `pdm` uses global project implicitly the following message is printed: `Project is not found, fallback to the global project`.\nTo disable this message set the config `global_project.fallback_verbose` to `false`.\n\nIf you want global project to track another project file other than `<CONFIG_ROOT>/global-project`,\nyou can provide the project path via `-p/--project <path>` option.\nEspecially if you pass `--global --project .`,\nPDM will install the dependencies of the current project into the global Python.\n\n!!! warning\n    Be careful with `remove` and `sync --clean/--pure` commands when global project is used, because it may remove packages installed in your system Python.\n"
  },
  {
    "path": "docs/usage/hooks.md",
    "content": "# Lifecycle and Hooks\n\nAs any Python deliverable, your project will go through the different phases\nof a Python project lifecycle and PDM provides commands to perform the expected tasks for those phases.\n\nIt also provides hooks attached to these steps allowing for:\n\n- plugins to listen to the [signals][pdm.signals] of the same name.\n- developers to define custom scripts with the same name.\n\nBesides, `pre_invoke` signal is emitted before ANY command is invoked, allowing plugins to modify the project or options beforehand.\n\nThe built-in commands are currently split into 3 groups:\n\n- the [initialization phase](#initialization)\n- the [dependencies management](#dependencies-management).\n- the [publication phase](#publication).\n\nYou will most probably need to perform some recurrent tasks between the installation and publication phases (housekeeping, linting, testing, ...)\nthis is why PDM lets you define your own tasks/phases using [user scripts](#user-scripts).\n\nTo provides full flexibility, PDM allows to [skip some hooks and tasks](#skipping) on demand.\n\n## Initialization\n\nThe initialization phase should occur only once in a project lifetime by running the [`pdm init`](../reference/cli.md#init)\ncommand to initialize an existing project (prompt to fill the `pyproject.toml` file).\n\nThey trigger the following hooks:\n\n- [`post_init`][pdm.signals.post_init]\n\n```mermaid\nflowchart LR\n  subgraph pdm-init [pdm init]\n    direction LR\n    post-init{{Emit post_init}}\n    init --> post-init\n  end\n```\n\n## Dependencies management\n\nThe dependencies management is required for the developer to be able to work and perform the following:\n\n- `lock`: compute a lock file from the `pyproject.toml` requirements.\n- `sync`: synchronize (add/remove/update) PEP582 packages from the lock file and install the current project as editable.\n- `add`: add a dependency\n- `remove`: remove a dependency\n\nAll those steps are directly available with the following commands:\n\n- [`pdm lock`](../reference/cli.md#lock): execute the `lock` task\n- [`pdm sync`](../reference/cli.md#sync): execute the `sync` task\n- [`pdm install`](../reference/cli.md#install): execute the `sync` task, preceded from `lock` if required\n- [`pdm add`](../reference/cli.md#add): add a dependency requirement, re-lock and then sync\n- [`pdm remove`](../reference/cli.md#remove): remove a dependency requirement, re-lock and then sync\n- [`pdm update`](../reference/cli.md#update): re-lock dependencies from their latest versions and then sync\n\nThey trigger the following hooks:\n\n- [`pre_install`][pdm.signals.pre_install]\n- [`post_install`][pdm.signals.post_install]\n- [`pre_lock`][pdm.signals.pre_lock]\n- [`post_lock`][pdm.signals.post_lock]\n\n```mermaid\nflowchart LR\n  subgraph pdm-install [pdm install]\n    direction LR\n\n    subgraph pdm-lock [pdm lock]\n      direction TB\n      pre-lock{{Emit pre_lock}}\n      post-lock{{Emit post_lock}}\n      pre-lock --> lock --> post-lock\n    end\n\n    subgraph pdm-sync [pdm sync]\n      direction TB\n      pre-install{{Emit pre_install}}\n      post-install{{Emit post_install}}\n      pre-install --> sync --> post-install\n    end\n\n    pdm-lock --> pdm-sync\n  end\n```\n\n### Switching Python version\n\nThis is a special case in dependency management:\nyou can switch the current Python version using [`pdm use`](../reference/cli.md#use)\nand it will emit the [`post_use`][pdm.signals.post_use] signal with the new Python interpreter.\n\n```mermaid\nflowchart LR\n  subgraph pdm-use [pdm use]\n    direction LR\n    post-use{{Emit post_use}}\n    use --> post-use\n  end\n```\n\n## Publication\n\nAs soon as you are ready to publish your package/library, you will require the publication tasks:\n\n- `build`: build/compile assets requiring it and package everything into a Python package (sdist, wheel)\n- `upload`: upload/publish the package to a remote PyPI index\n\nAll those steps are available with the following commands:\n\n- [`pdm build`](../reference/cli.md#build)\n- [`pdm publish`](../reference/cli.md#publish)\n\nThey trigger the following hooks:\n\n- [`pre_publish`][pdm.signals.pre_publish]\n- [`post_publish`][pdm.signals.post_publish]\n- [`pre_build`][pdm.signals.pre_build]\n- [`post_build`][pdm.signals.post_build]\n\n```mermaid\nflowchart LR\n  subgraph pdm-publish [pdm publish]\n    direction LR\n    pre-publish{{Emit pre_publish}}\n    post-publish{{Emit post_publish}}\n\n    subgraph pdm-build [pdm build]\n      pre-build{{Emit pre_build}}\n      post-build{{Emit post_build}}\n      pre-build --> build --> post-build\n    end\n\n    %% subgraph pdm-upload [pdm upload]\n    %%   pre-upload{{Emit pre_upload}}\n    %%   post-upload{{Emit post_upload}}\n    %%   pre-upload --> upload --> post-upload\n    %% end\n\n    pre-publish --> pdm-build --> upload --> post-publish\n  end\n```\n\nExecution will stop at first failure, hooks included.\n\n## User scripts\n\n[User scripts are detailed in their own section](scripts.md) but you should know that:\n\n- each user script can define a `pre_*` and `post_*` script, including composite scripts.\n- each `run` execution will trigger the [`pre_run`][pdm.signals.pre_run] and [`post_run`][pdm.signals.post_run] hooks\n- each script execution will trigger the [`pre_script`][pdm.signals.pre_script] and [`post_script`][pdm.signals.post_script] hooks\n\nGiven the following `scripts` definition:\n\n```toml\n[tool.pdm.scripts]\npre_script = \"\"\npost_script = \"\"\npre_test = \"\"\npost_test = \"\"\ntest = \"\"\npre_composite = \"\"\npost_composite = \"\"\ncomposite = {composite = [\"test\"]}\n```\n\na `pdm run test` will have the following lifecycle:\n\n```mermaid\nflowchart LR\n  subgraph pdm-run-test [pdm run test]\n    direction LR\n    pre-run{{Emit pre_run}}\n    post-run{{Emit post_run}}\n    subgraph run-test [test task]\n      direction TB\n      pre-script{{Emit pre_script}}\n      post-script{{Emit post_script}}\n      pre-test[Execute pre_test]\n      post-test[Execute post_test]\n      test[Execute test]\n\n      pre-script --> pre-test --> test --> post-test --> post-script\n    end\n\n    pre-run --> run-test --> post-run\n  end\n```\n\nwhile `pdm run composite` will have the following:\n\n```mermaid\nflowchart LR\n  subgraph pdm-run-composite [pdm run composite]\n    direction LR\n    pre-run{{Emit pre_run}}\n    post-run{{Emit post_run}}\n\n    subgraph run-composite [composite task]\n      direction TB\n      pre-script-composite{{Emit pre_script}}\n      post-script-composite{{Emit post_script}}\n      pre-composite[Execute pre_composite]\n      post-composite[Execute post_composite]\n\n      subgraph run-test [test task]\n        direction TB\n        pre-script-test{{Emit pre_script}}\n        post-script-test{{Emit post_script}}\n        pre-test[Execute pre_test]\n        post-test[Execute post_test]\n\n        pre-script-test --> pre-test --> test --> post-test --> post-script-test\n      end\n\n      pre-script-composite --> pre-composite --> run-test --> post-composite --> post-script-composite\n    end\n\n     pre-run --> run-composite --> post-run\n  end\n```\n\n## Skipping\n\nIt is possible to control which task and hook runs for any built-in command as well as custom user scripts using the `--skip` option.\n\nIt accepts a comma-separated list of hooks/task names to skip\nas well as the predefined `:all`, `:pre` and `:post` shortcuts\nrespectively skipping all hooks, all `pre_*` hooks and all `post_*` hooks.\nYou can also provide the skip list in `PDM_SKIP_HOOKS` environment variable\nbut it will be overridden as soon as the `--skip` parameter is provided.\n\nGiven the previous script block, running `pdm run --skip=:pre,post_test composite` will result in the following reduced lifecycle:\n\n```mermaid\nflowchart LR\n  subgraph pdm-run-composite [pdm run composite]\n    direction LR\n    post-run{{Emit post_run}}\n\n    subgraph run-composite [composite task]\n      direction TB\n      post-script-composite{{Emit post_script}}\n      post-composite[Execute post_composite]\n\n      subgraph run-test [test task]\n        direction TB\n        post-script-test{{Emit post_script}}\n\n        test --> post-script-test\n      end\n\n      run-test --> post-composite --> post-script-composite\n    end\n\n     run-composite --> post-run\n  end\n```\n"
  },
  {
    "path": "docs/usage/lock-targets.md",
    "content": "# Lock for specific platforms or Python versions\n\n+++ 2.17.0\n\nBy default, PDM will try to make a lock file that works on all platforms within the Python versions specified by [`requires-python` in `pyproject.toml`](./project.md#specify-requires-python). This is very convenient during development. You can generate a lock file in your development environment and then use this lock file to replicate the same dependency versions in CI/CD or production environments.\n\nHowever, there are times when this approach may not work. For example, your project or dependency has some platform-specific dependencies, or conditional dependencies depending on the Python version, like the following:\n\n```toml\n[project]\nname = \"myproject\"\nrequires-python = \">=3.9\"\ndependencies = [\n    \"numpy<1.25; python_version < '3.9'\",\n    \"numpy>=1.25; python_version >= '3.9'\",\n    \"pywin32; sys_platform == 'win32'\",\n]\n```\n\nIn this case, it's almost impossible to get a single resolution for each package on all platforms and Python versions(`>=3.9`). You should, instead, make lock files for specific platforms or Python versions.\n\n## Specify lock target when generating lock file\n\nPDM supports specifying one or more environment criteria when generating a lock file. These criteria include:\n\n- `--python=<PYTHON_RANGE>`: A [PEP 440](https://www.python.org/dev/peps/pep-0440/) compatible Python version specifier. For example, `--python=\">=3.9,<3.10\"` will generate a lock file for Python versions `>=3.9` and `<3.10`. For convenience, `--python=3.10` is equivalent to `--python=\">=3.10\"`, meaning to resolve for Python 3.10 and above.\n- `--platform=<PLATFORM>`: A platform specifier. For example, `pdm lock --platform=linux` will generate a lock file for Linux x86_64 platform. Available options are:\n    * `linux`\n    * `windows`\n    * `macos`\n    * `alpine`\n    * `windows_amd64`\n    * `windows_x86`\n    * `windows_arm64`\n    * `macos_arm64`\n    * `macos_x86_64`\n    * `macos_X_Y_arm64`\n    * `macos_X_Y_x86_64`\n    * `manylinux_X_Y_x86_64`\n    * `manylinux_X_Y_aarch64`\n    * `musllinux_X_Y_x86_64`\n    * `musllinux_X_Y_aarch64`\n- `--implementation=cpython|pypy|pyston`: A Python implementation specifier. Currently only `cpython`, `pypy`, and `pyston` are supported.\n\nYou can ignore some of the criteria, for example, by specifying only `--platform=linux`, the generated lock file will be applicable to Linux platform and all implementations.\n\n!!! note \"`python` criterion and `requires-python`\"\n\n    `--python` option, or `requires-python` criterion in the lock target is still limited by the `requires-python` in `pyproject.toml`. For example, if `requires-python` is `>=3.9` and you specified `--python=\"<3.11\"`, the lock target will be `>=3.9,<3.11`.\n\n## Separate lock files or merge into one\n\nIf you need more than one lock targets, you can either create separate lock files for each target or combine them into a single lock file. PDM supports both ways.\n\nTo create separate lock file with a specific target:\n\n```bash\n# Generate a lock file for Linux platform and Python 3.9, write the result to py38-linux.lock\npdm lock --platform=linux --python=\"==3.9.*\" --lockfile=py38-linux.lock\n```\n\nWhen you install dependencies on Linux and Python 3.9, you can use this lock file:\n\n```bash\npdm install --lockfile=py38-linux.lock\n```\n\nAdditionally, you can also select a subset of dependency groups for the lock file, see [here](./lockfile.md#specify-another-lock-file-to-use) for more details.\n\nIf you would like to use the same lock file for multiple targets, add `--append` to the `pdm lock` command:\n\n```bash\n# Generate a lock file for Linux platform and Python 3.9, append the result to pdm.lock\npdm lock --platform=linux --python=\"==3.9.*\" --append\n```\n\nThe advantages of using a single lock file are you don't need to manage multiple lock files when updating dependencies. However, you can't specify different lock strategies for different targets in a single lock file. And the time cost of updating the locks is expected to be higher.\n\nWhat's more, each lock file can have one or more lock targets, making it rather flexible to use. You can choose to merge some targets in a lock file and lock specific groups and targets in separate lock files. We'll illustrate this with an example in the next section.\n\n## Example\n\nHere is the `pyproject.toml` content:\n\n```toml\n[project]\nname = \"myproject\"\nrequires-python = \">=3.9\"\ndependencies = [\n    \"numpy<1.25; python_version < '3.10'\",\n    \"numpy>=1.25; python_version >= '3.10'\",\n    \"pandas\"\n]\n\n[project.optional-dependencies]\nwindows = [\"pywin32\"]\nmacos = [\"pyobjc\"]\n```\n\nIn the above example, we have conditional dependency versions for `numpy` and platform-specific optional dependencies for Windows and MacOS. We want to generate lock files for Linux, Windows, and MacOS platforms, and Python 3.9 and 3.10.\n\n```bash\npdm lock --python=\">=3.10\"\npdm lock --python=\"<3.10\" --append\n\npdm lock --platform=windows --python=\">=3.10\" --lockfile=py310-windows.lock --with windows\npdm lock --platform=macos --python=\">=3.10\" --lockfile=py310-macos.lock --with macos\n```\nRun the above commands in order, and you will get 3 lockfiles:\n\n- `pdm.lock`: the default main lock file, which works on all platforms and Python versions in `>=3.9`. No platform specific dependencies are included. In this lock file, there are two versions of `numpy`, suitable for Python 3.10 and above and below respectively. The PDM installer will choose the correct version according to the Python version.\n- `py39-windows.lock`: lock file for Windows platform and Python 3.10 above, including the optional dependencies for Windows.\n- `py39-macos.lock`: lock file for MacOS platform and Python 3.10 above, including the optional dependencies for MacOS.\n"
  },
  {
    "path": "docs/usage/lockfile.md",
    "content": "# Lock file\n\nPDM installs packages exclusively from the existing lock file named `pdm.lock`. This file serves as the sole source of truth for installing dependencies. The lock file contains essential information such as:\n\n- All packages and their versions\n- The file names and hashes of the packages\n- Optionally, the origin URLs to download the packages (See also: [Static URLs](#static-urls))\n- The dependencies and markers of each package (See also: [Inherit the metadata from parents](#inherit-the-metadata-from-parents))\n\nTo create or overwrite the lock file, run [`pdm lock`](../reference/cli.md#lock), and it supports the same [update strategies](./dependency.md#about-update-strategy) as [`pdm add`](../reference/cli.md#add). In addition, the [`pdm install`](../reference/cli.md#install) and [`pdm add`](../reference/cli.md#add) commands will also automatically create the `pdm.lock` file.\n\n??? note \"Should I add `pdm.lock` to version control?\"\n\n    It depends. If your goal is to make CI use the same dependency versions as local development and avoid unexpected failures, you should add the `pdm.lock` file to version control. Otherwise, if your project is a library and you want CI to mimic the installation on user site to ensure that the current version on PyPI doesn't break anything, then do not submit the `pdm.lock` file.\n\n## Install the packages pinned in lock file\n\nThere are a few similar commands to do this job with slight differences:\n\n- [`pdm sync`](../reference/cli.md#sync) installs packages from the lock file.\n- [`pdm update`](../reference/cli.md#update) will update the lock file, then `pdm sync`.\n- [`pdm install`](../reference/cli.md#install) will check the project file for changes, update the lock file if needed, then `pdm sync`.\n\n`pdm sync` also has a few options to manage installed packages:\n\n- `--clean`: will remove packages no longer in the lockfile\n- `--clean-unselected` (or `--only-keep`): more thorough version of `--clean` that will also remove packages not in the groups specified by the `-G`, `-d`, and `--prod` options.\n  Note: by default, `pdm sync` selects all groups from the lockfile, so `--clean-unselected` is identical to `--clean` unless `-G`, `-d`, and `--prod` are used.\n\n## Hashes in the lock file\n\nBy default, `pdm install` will check if the lock file matches the content of `pyproject.toml`, this is done by storing a content hash of `pyproject.toml` in the lock file.\n\nTo check if the hash in the lock file is up-to-date:\n\n```bash\npdm lock --check\n```\n\nIf you want to refresh the lock file without changing the dependencies, you can use the `--refresh` option:\n\n```bash\npdm lock --refresh\n```\n\nThis command also refreshes _all_ file hashes recorded in the lock file.\n\n## Change lock file format\n\nPDM supports two lock file formats: `pdm`(default file name is `pdm.lock`) and `pylock`(default file name is `pylock.toml`). The default format is `pdm`.\n\n+++ 2.25.0\n\n    Added experimental support for the [PEP 751](https://packaging.python.org/en/latest/specifications/pylock-toml/#pylock-toml-spec) pylock file format. It's a standard lock file format designed to minimize discrepancies among different Python package managers, enhancing interoperability with other tools. It is set to become the default in a future version of PDM. Read the specification for more details.\n\nYou can switch to the `pylock` format with `pdm config` command:\n\n```bash\npdm config lock.format pylock\n```\n\n## Specify another lock file to use\n\nBy default, PDM uses `pdm.lock` in the current directory. You can specify another lock file with the `-L/--lockfile` option or the `PDM_LOCKFILE` environment variable:\n\n```bash\npdm install --lockfile my-lockfile.lock\n```\n\nThis command installs packages from `my-lockfile.lock` instead of `pdm.lock`.\n\nAlternate lock files are helpful when there exist conflicting dependencies for different environments. In this case, if you lock them as a whole, PDM will raise an error. So you have to [select a subset of dependency groups](./dependency.md#select-a-subset-of-dependency-groups-to-install) and lock them separately.\n\nFor a realistic example, your project depends on a release version of `werkzeug` and you may want to work with a local in-development copy of it when developing. You can add the following to your `pyproject.toml`:\n\n```toml\n[project]\nrequires-python = \">=3.7\"\ndependencies = [\"werkzeug\"]\n\n[dependency-groups]\ndev = [\"werkzeug @ file:///${PROJECT_ROOT}/dev/werkzeug\"]\n```\n\nThen, run `pdm lock` with different options to generate lockfiles for different purposes:\n\n```bash\n# Lock default + dev, write to pdm.lock\n# with the local copy of werkzeug pinned.\npdm lock\n# Lock default, write to pdm.prod.lock\n# with the release version of werkzeug pinned.\npdm lock --prod -L pdm.prod.lock\n```\n\nCheck the `metadata.groups` field in the lockfile to see which groups are included.\n\n## Option to not write lock file\n\nSometimes you want to add or update dependencies without updating the lock file, or you don't want to generate `pdm.lock`, you can use the `--frozen-lockfile` option:\n\n```bash\npdm add --frozen-lockfile flask\n```\n\nIn this case, the lock file, if existing, will become read-only, no write operation will be performed on it.\nHowever, dependency resolution step will still be performed if needed.\n\n## Lock strategies\n\nCurrently, we support three flags to control the locking behavior: `cross_platform`, `static_urls` and `direct_minimal_versions`, with the meanings as follows.\nYou can pass one or more flags to `pdm lock` by `--strategy/-S` option, either by giving a comma-separated list or by passing the option multiple times.\nBoth of these commands function in the same way:\n\n```bash\npdm lock -S cross_platform,static_urls\npdm lock -S cross_platform -S static_urls\n```\n\nThe flags will be encoded in the lockfile and get read when you run `pdm lock` next time. But you can disable flags by prefixing the flag name with `no_`:\n\n```bash\npdm lock -S no_cross_platform\n```\n\nThis command makes the lockfile not cross-platform.\n\n### Cross platform\n\n+++ 2.6.0\n\n!!! warning \"Deprecated in 2.17.0\"\n    See [Lock for specific platforms or Python versions](./lock-targets.md) for the new behavior.\n\nBy default, the generated lockfile is **cross-platform**, which means the current platform isn't taken into account when resolving the dependencies. The result lockfile will contain wheels and dependencies for all possible platforms and Python versions.\nHowever, sometimes this will result in a wrong lockfile when a release doesn't contain all wheels.\nTo avoid this, you can tell PDM to create a lockfile that works for **this platform** only, trimming the wheels not relevant to the current platform.\nThis can be done by passing the `--strategy no_cross_platform` option to `pdm lock`:\n\n```bash\npdm lock --strategy no_cross_platform\n```\n\n### Static URLs\n\n+++ 2.8.0\n\nBy default, PDM only stores the filenames of the packages in the lockfile, which benefits the reusability across different package indexes.\nHowever, if you want to store the static URLs of the packages in the lockfile, you can pass the `--strategy static_urls` option to `pdm lock`:\n\n```bash\npdm lock --strategy static_urls\n```\n\nThe settings will be saved and remembered for the same lockfile. You can also pass `--strategy no_static_urls` to disable it.\n\n### Direct minimal versions\n\n+++ 2.10.0\n\nWhen it is enabled by passing `--strategy direct_minimal_versions`, dependencies specified in the `pyproject.toml` will be resolved to the minimal versions available, rather than the latest versions. This is useful when you want to test the compatibility of your project within a range of dependency versions.\n\nFor example, if you specified `flask>=2.0` in the `pyproject.toml`, `flask` will be resolved to version `2.0.0` if there is no other compatibility issue.\n\n!!! note\n    Version constraints in package dependencies are not future-proof. If you resolve the dependencies to the minimal versions, there will likely be backwards-compatibility issues.\n    For example, `flask==2.0.0` requires `werkzeug>=2.0`, but in fact, it can not work with `Werkzeug 3.0.0`, which is released 2 years after it.\n\n### Inherit the metadata from parents\n\n+++ 2.11.0\n\nPreviously, the `pdm lock` command would record package metadata as it is. When installing, PDM would start from the top requirements and traverse down to the leaf node of the dependency tree. It would then evaluate any marker it encounters against the current environment. If a marker is not satisfied, the package would be discarded. In other words, we need an additional \"resolution\" step in installation.\n\nWhen the `inherit_metadata` strategy is enabled, PDM will inherit and merge environment markers from a package's ancestors. These markers are then encoded in the lockfile during locking, resulting in faster installations. This has been enabled by default from version `2.11.0`, to disable this strategy in the config, use `pdm config strategy.inherit_metadata false`.\n\n### Exclude packages newer than specific date\n\n+++ 2.13.0\n\nYou can exclude packages that are newer than a specified date by passing the `--exclude-newer` option to `pdm lock`. This is useful when you want to lock the dependencies to a specific date, for example, to ensure reproducibility of the build.\n\nThe date may be specified as a RFC 3339 timestamp (e.g., `2006-12-02T02:07:43Z`) or UTC date in the same format (e.g., `2006-12-02`).\n\n```bash\npdm lock --exclude-newer 2024-01-01\n```\n\n!!! note\n    The package index must support the `upload-time` field as specified in [PEP 700]. If the field is not present for a given distribution, the distribution will be treated as unavailable.\n\n[PEP 700]: https://peps.python.org/pep-0700/\n\n## Set acceptable format for locking or installing\n\nIf you want to control the format(binary/sdist) of the packages, you can set the env vars `PDM_NO_BINARY`, `PDM_ONLY_BINARY` and `PDM_PREFER_BINARY`.\n\nEach env var is a comma-separated list of package name. You can set it to `:all:` to apply to all packages. For example:\n\n```toml\n# No binary for werkzeug will be locked nor used for installation\nPDM_NO_BINARY=werkzeug pdm add flask\n# Only binaries will be locked in the lock file\nPDM_ONLY_BINARY=:all: pdm lock\n# No binaries will be used for installation\nPDM_NO_BINARY=:all: pdm install\n# Prefer binary distributions and even if sdist with higher version is available\nPDM_PREFER_BINARY=flask pdm install\n```\n\nYou can also defined those values in your project `pyproject.toml` with the `no-binary`, `only-binary` and `prefer-binary` keys of the `tool.pdm.resolution` section.\nThey accept the same format as the environment variables and also support lists.\n\n```toml\n[tool.pdm.resolution]\n# No binary for werkzeug and flask will be locked nor used for installation\nno-binary = \"werkzeug,flask\"\n# equivalent to\nno-binary = [\"werkzeug\", \"flask\"]\n# Only binaries will be locked in the lock file\nonly-binary = \":all:\"\n# Prefer binary distributions and even if sdist with higher version is available\nprefer-binary = \"flask\"\n```\n\n!!! note\n    Each environment variable takes precedence over its `pyproject.toml` alternative.\n\n## Allow prerelease versions to be installed\n\nInclude the following setting in `pyproject.toml` to enable:\n\n```toml\n[tool.pdm.resolution]\nallow-prereleases = true\n```\n\n## Solve the locking failure\n\nIf PDM is not able to find a resolution to satisfy the requirements, it will raise an error. For example,\n\n```bash\npdm django==3.1.4 \"asgiref<3\"\n...\n🔒 Lock failed\nUnable to find a resolution for asgiref because of the following conflicts:\n    asgiref<3 (from project)\n    asgiref<4,>=3.2.10 (from <Candidate django 3.1.4 from https://pypi.org/simple/django/>)\nTo fix this, you could loosen the dependency version constraints in pyproject.toml. If that is not possible, you could also override the resolved version in `[tool.pdm.resolution.overrides]` table.\n```\n\nYou can either change to a lower version of `django` or remove the upper bound of `asgiref`. But if it is not eligible for your project, you can try [overriding the resolved package versions](./config.md#override-the-resolved-package-versions) or even [don't lock that specific package](./config.md#exclude-specific-packages-and-their-dependencies-from-the-lock-file) in `pyproject.toml`.\n\n## Export locked packages to alternative formats\n\nYou can export the `pdm.lock` file to other formats, which will simplify the CI flow or image building process. At present, only the `requirements.txt` format is supported.\n\n```bash\npdm export -o requirements.txt\n```\n\n!!! tip\n    You can also run `pdm export` with a [`.pre-commit` hook](./advanced.md#hooks-for-pre-commit).\n\n+++ 2.24.0\n\nAdditionally, PDM supports exporting to `pylock.toml` format as defined by [PEP 751](https://packaging.python.org/en/latest/specifications/pylock-toml/#pylock-toml-spec). The following command will convert your lock file to a PEP 751 compatible format:\n\n```bash\npdm export -f pylock -o pylock.toml\n```\n"
  },
  {
    "path": "docs/usage/pep582.md",
    "content": "# Working with PEP 582\n\n!!! warning \"PEP 582 has been rejected\"\n    This is a rejected PEP. However, due to the fact that this feature is the reason for PDM's birth, PDM will retain the support.\n    We recommend using [virtual environments](./venv.md) instead.\n\nWith [PEP 582](https://www.python.org/dev/peps/pep-0582/), dependencies will be installed into `__pypackages__` directory under the project root. With [PEP 582 enabled globally](#enable-pep-582-globally), you can also use the project interpreter to run scripts directly.\n\n**When the project interpreter is a normal Python, this mode is enabled.**\n\nBesides, on a project you work with for the first time on your machine, if it contains an empty `__pypackages__` directory, PEP 582 is enabled automatically, and virtualenv won't be created.\n\n## Enable PEP 582 in projects managed my pdm\n\nTo make pdm use PEP 582 instead of virtual environment, set `python.use_venv` config variable to False:\n\n```bash\npdm config python.use_venv False\n```\n\n## Enable PEP 582 globally\n\nTo make the Python interpreters aware of PEP 582 packages,\none needs to add the `pdm/pep582/sitecustomize.py` to the Python library search path.\n\n=== \"Windows\"\n\n    One just needs to execute `pdm --pep582`, then environment variable will be changed automatically. Don't forget\n    to restart the terminal session to take effect.\n\n=== \"Mac and Linux\"\n\n    The command to change the environment variables can be printed by `pdm --pep582 [<SHELL>]`.\n    If `<SHELL>` isn't given, PDM will pick one based on some guesses. \n    You can run `eval \"$(pdm --pep582)\"` to execute the command.\n\n    You may want to write a line in your `.bash_profile`(or similar profiles) to make it effective when logging in.\n    For example, in bash you can do this:\n\n    ```bash\n    pdm --pep582 >> ~/.bash_profile\n    ```\n\n    Once again, Don't forget to restart the terminal session to take effect.\n\n??? note \"How is it done?\"\n\n    Thanks to the [site packages loading](https://docs.python.org/3/library/site.html) on Python startup. It is possible to patch the `sys.path`\n    by executing the `sitecustomize.py` shipped with PDM. The interpreter can search the directories\n    for the nearest `__pypackage__` folder and append it to the `sys.path` variable.\n\n## Configure IDE to support PEP 582\n\nNow there are no built-in support or plugins for PEP 582 in most IDEs, you have to configure your tools manually.\n\n### PyCharm\n\nMark `__pypackages__/<major.minor>/lib` as [Sources Root](https://www.jetbrains.com/help/pycharm/configuring-project-structure.html#mark-dir-project-view).\nThen, select as [Python interpreter](https://www.jetbrains.com/help/pycharm/configuring-python-interpreter.html#interpreter) a Python installation with the same `<major.minor>` version.\n\nAdditionally, if you want to use tools from the environment (e.g. `pytest`), you have to add the\n`__pypackages__/<major.minor>/bin` directory to the `PATH` variable in the corresponding run/debug configuration.\n\n### VSCode\n\nAdd the following two entries to the top-level dict in `.vscode/settings.json`:\n\n```json\n{\n  \"python.autoComplete.extraPaths\": [\"__pypackages__/<major.minor>/lib\"],\n  \"python.analysis.extraPaths\": [\"__pypackages__/<major.minor>/lib\"]\n}\n```\n\nThis file can be auto-generated with plugin [`pdm-vscode`](https://github.com/frostming/pdm-vscode).\n\n[Enable PEP582 globally](#enable-pep-582-globally),\nand make sure VSCode runs using the same user and shell you enabled PEP582 for.\n\n??? note \"Cannot enable PEP582 globally?\"\n    If for some reason you cannot enable PEP582 globally, you can still configure each \"launch\" in each project:\n    set the `PYTHONPATH` environment variable in your launch configuration, in `.vscode/launch.json`.\n    For example, to debug your `pytest` run:\n\n    ```json\n    {\n        \"version\": \"0.2.0\",\n        \"configurations\": [\n            {\n                \"name\": \"pytest\",\n                \"type\": \"python\",\n                \"request\": \"launch\",\n                \"module\": \"pytest\",\n                \"args\": [\"tests\"],\n                \"justMyCode\": false,\n                \"env\": {\"PYTHONPATH\": \"__pypackages__/<major.minor>/lib\"}\n            }\n        ]\n    }\n    ```\n\n    If your package resides in a `src` directory, add it to `PYTHONPATH` as well:\n\n    ```json\n    \"env\": {\"PYTHONPATH\": \"src:__pypackages__/<major.minor>/lib\"}\n    ```\n\n??? note \"Using Pylance/Pyright?\"\n    If you have configured `\"python.analysis.diagnosticMode\": \"workspace\"`,\n    and you see a ton of errors/warnings as a result.\n    you may need to create `pyrightconfig.json` in the workspace directory, and fill in the following fields:\n\n    ```json\n    {\n        \"exclude\": [\"__pypackages__\"]\n    }\n    ```\n\n    Then restart the language server or VS Code and you're good to go.\n    In the future ([microsoft/pylance-release#1150](https://github.com/microsoft/pylance-release/issues/1150)), maybe the problem will be solved.\n\n??? note \"Using Jupyter Notebook?\"\n    If you wish to use pdm to install jupyter notebook and use it in vscode in conjunction with the python extension:\n\n    1. Use `pdm add notebook` or so to install notebook\n    2. Add a `.env` file inside of your project directory with contents like the following:\n\n    ```\n    PYTHONPATH=/your-workspace-path/__pypackages__/<major>.<minor>/lib\n    ```\n\n    If the above still doesn't work, it's most likely because the environment variable is not properly loaded when the Notebook starts. There are two workarounds.\n\n    1. Run `code .` in Terminal. It will open a new VSCode window in the current directory with the path set correctly. Use the Jupyter Notebook in the new window\n    2. If you prefer not to open a new window, run the following at the beginning of your Jupyter Notebook to explicitly set the path:\n\n    ```\n    import sys\n    sys.path.append('/your-workspace-path/__pypackages__/<major>.<minor>/lib')\n    ```\n\n    > [Reference Issue](https://github.com/pdm-project/pdm/issues/848)\n\n??? note \"PDM Task Provider\"\n\n    In addition, there is a [VSCode Task Provider extension][pdm task provider] available for download.\n\n    This makes it possible for VSCode to automatically detect [pdm scripts][pdm scripts] so they\n    can be run natively as [VSCode Tasks][vscode tasks].\n\n    [vscode tasks]: https://code.visualstudio.com/docs/editor/tasks\n    [pdm task provider]: https://marketplace.visualstudio.com/items?itemName=knowsuchagency.pdm-task-provider\n    [pdm scripts]: scripts.md\n\n### Neovim\n\nIf using [neovim-lsp](https://github.com/neovim/nvim-lspconfig) with\n[pyright](https://github.com/Microsoft/pyright) and want your `__pypackages__` directory to be added to the path,\nyou can add this to your project's `pyproject.toml`.\n\n```toml\n[tool.pyright]\nextraPaths = [\"__pypackages__/<major.minor>/lib/\"]\n```\n\n### Emacs\n\nYou have a few options, but basically you'll want to tell an LSP client to add `__pypackages__` to the paths it looks at. Here are a few options that are available:\n\n#### Using `pyproject.toml` and pyright\n\nAdd this to your project's `pyproject.toml`:\n\n```toml\n[tool.pyright]\nextraPaths = [\"__pypackages__/<major.minor>/lib/\"]\n```\n\n#### eglot + pyright\n\nUsing [pyright](https://github.com/microsoft/pyright) and [eglot](https://github.com/joaotavora/eglot) (included in Emacs 29), add the following to your config:\n\n```emacs-lisp\n(defun get-pdm-packages-path ()\n  \"For the current PDM project, find the path to the packages.\"\n  (let ((packages-path (string-trim (shell-command-to-string \"pdm info --packages\"))))\n    (concat packages-path \"/lib\")))\n\n(defun my/eglot-workspace-config (server)\n  \"For the current PDM project, dynamically generate a python lsp config.\"\n  `(:python\\.analysis (:extraPaths ,(vector (get-pdm-packages-path)))))\n\n(setq-default eglot-workspace-configuration #'my/eglot-workspace-config)\n```\n\nYou'll want pyright installed either globally, or in your project (probably as a dev dependency). You can add this with, for example:\n\n```bash\npdm add --dev --group devel pyright\n```\n\n#### LSP-Mode + lsp-python-ms\n\nBelow is a sample code snippet showing how to make PDM work with [lsp-python-ms](https://github.com/emacs-lsp/lsp-python-ms) in Emacs. Contributed by [@linw1995](https://github.com/pdm-project/pdm/discussions/372#discussion-3303501).\n\n```emacs-lisp\n  ;; TODO: Cache result\n  (defun linw1995/pdm-get-python-executable (&optional dir)\n    (let ((pdm-get-python-cmd \"pdm info --python\"))\n      (string-trim\n       (shell-command-to-string\n        (if dir\n            (concat \"cd \"\n                    dir\n                    \" && \"\n                    pdm-get-python-cmd)\n          pdm-get-python-cmd)))))\n\n  (defun linw1995/pdm-get-packages-path (&optional dir)\n    (let ((pdm-get-packages-cmd \"pdm info --packages\"))\n      (concat (string-trim\n               (shell-command-to-string\n                (if dir\n                    (concat \"cd \"\n                            dir\n                            \" && \"\n                            pdm-get-packages-cmd)\n                  pdm-get-packages-cmd)))\n              \"/lib\")))\n\n  (use-package lsp-python-ms\n    :ensure t\n    :init (setq lsp-python-ms-auto-install-server t)\n    :hook (python-mode\n           . (lambda ()\n               (setq lsp-python-ms-python-executable (linw1995/pdm-get-python-executable))\n               (setq lsp-python-ms-extra-paths (vector (linw1995/pdm-get-packages-path)))\n               (require 'lsp-python-ms)\n               (lsp))))  ; or lsp-deferred\n```\n"
  },
  {
    "path": "docs/usage/project.md",
    "content": "# New Project\n\nTo start with, create a new project with [`pdm new`](../reference/cli.md#new):\n\n```bash\npdm new my-project\n```\n\nYou will need to answer a few questions, to help PDM to create a `pyproject.toml` file for you.\nFor more usages of `pdm new`, please read [Create your project from a template](./template.md).\n\n## Create pyproject.toml for an existing project\n\nIf you already have a project and want to create a `pyproject.toml` file for it, you can use [`pdm init`](../reference/cli.md#init):\n\n```bash\ncd my-project\npdm init\n```\n\n## Choose a Python interpreter\n\nAt first, you need to choose a Python interpreter from a list of Python versions installed on your machine. The interpreter path\nwill be stored in `.pdm-python` and used by subsequent commands. You can also change it later with [`pdm use`](../reference/cli.md#use).\n\nAlternatively, you can specify the Python interpreter path via `PDM_PYTHON` environment variable. When it is set, the path saved in `.pdm-python` will be ignored.\n\n+++ 2.23.0\n\nIf `.python-version` is present in the project root or `PDM_PYTHON_VERSION` env var is set, PDM will use the Python version specified in it. The file or env var should contain a valid Python version string, such as `3.11`.\n\n!!! warning \"Using an existing environment\"\n    If you choose to use an existing environment, such as reusing an environment created by `conda`, please note that PDM will _remove_ dependencies not listed in `pyproject.toml` or `pdm.lock` when running `pdm sync --clean` or `pdm remove`. This may lead to destructive consequences. Therefore, try not to share environment among multiple projects.\n\n### Install Python interpreters with PDM\n\n+++ 2.13.0\n\nPDM supports installing additional Python interpreters from [@indygreg's python-build-standalone](https://github.com/indygreg/python-build-standalone)\nwith the `pdm python install` command. For example, to install CPython 3.9.8:\n\n```bash\npdm python install 3.9.8\n```\n\nYou can view all available Python versions with `pdm python install --list`.\n\nThis will install the Python interpreter into the location specified by `python.install_root` configuration.\n\nList the currently installed Python interpreters:\n\n```bash\npdm python list\n```\n\nRemove an installed Python interpreter:\n\n```bash\npdm python remove 3.9.8\n```\n\nInstall a free-threaded Python interpreter:\n\n```bash\npdm python install 3.13t\n```\n\n!!! tip \"Share installations with Rye\"\n\n    PDM installs Python interpreters using the same source as [Rye](https://rye-up.com). If you are using Rye at the same time, you can point the `python.install_root` to the same directory as Rye to share the Python interpreters:\n\n    ```bash\n    pdm config python.install_root ~/.rye/py\n    ```\n\n    Afterwards you can manage the installations using either `rye toolchain` or `pdm python`.\n\n### Installation strategy based on `requires-python`\n\n+++ 2.16.0\n\nIf Python `version` is not given, PDM will try to install the best match for the current platform/arch combination\nbased on `requires-python` from `pyproject.toml` (if pyproject.toml or requires-python attribute is not available,\nall install-able Python interpreters are considered).\n\nDefault strategy is `maximum`, i.e. the highest cPython interpreter version will be installed.\n\nIf `minimum` is preferred, use the option `--min` and leave `version` empty.\n\n```bash\npdm python install --min\n```\n\nThe same principles apply to [`pdm use`](../reference/cli.md#use) (incl. an automatic installation feature)\nwhich make it a good unattended set up command for CI/CD or 'fresh start with existing pyproject.toml' use-cases.\n\n### Virtualenv or not\n\nAfter you select the Python interpreter, PDM will ask you whether you want to create a virtual environment for the project.\nIf you choose **yes**, PDM will create a virtual environment in the project root directory, and use it as the Python interpreter for the project.\n\nIf the selected Python interpreter is in a virtual environment, PDM will use it as the project environment and install dependencies into it.\nOtherwise, `__pypackages__` will be created in the project root and dependencies will be installed into it.\n\nFor the difference between these two approaches, please refer to the corresponding sections in the docs:\n\n- [Virtualenv](./venv.md)\n- [`__pypackages__`(PEP 582)](./pep582.md)\n\n## Library or Application\n\nA library and an application differ in many ways. In short, a library is a package that is intended to be installed and used by other projects. In most cases it also needs to be uploaded to PyPI. An application, on the other hand, is one that is directly facing end users and may need to be deployed into some production environments.\n\nIn PDM, if you choose to create a library, PDM will add a `name`, `version` field to the `pyproject.toml` file, as well as a `[build-system]` table for the [build backend](../reference/build.md), which is only useful if your project needs to be built and distributed. So you need to manually add these fields to `pyproject.toml` if you want to change the project from an application to a library. Also, a library project will be installed into the environment when you run `pdm install` or `pdm sync`, unless `--no-self` is specified.\n\nIn `pyproject.toml`, there is a field `distribution` under the `[tool.pdm]` table. If it is set to true, PDM will treat the project as a library.\n\n## Specify `requires-python`\n\nYou need to set an appropriate `requires-python` value for your project. This is an important property that affects how dependencies are resolved. Basically, each package's `requires-python` must _cover_ the project's `requires-python` range. For example, consider the following setup:\n\n- Project: `requires-python = \">=3.9\"`\n- Package `foo`: `requires-python = \">=3.7,<3.11\"`\n\nResolving the dependencies will cause a `ResolutionImpossible`:\n\n```bash\nUnable to find a resolution because the following dependencies don't work\non all Python versions defined by the project's `requires-python`\n```\n\nBecause the dependency's `requires-python` is `>=3.7,<3.11`, it _doesn't_ cover the project's `requires-python` range of `>=3.9`. In other words, the project promises to work on Python 3.9, 3.10, 3.11 (and so on), but the dependency doesn't support Python 3.11 (or any higher). Since PDM creates a cross-platform lockfile that should work on all Python versions within the `requires-python` range, it can't find a valid resolution.\nTo fix this, you need add a maximum version to `requires-python`, like `>=3.9,<3.11`.\n\nThe value of `requires-python` is a [version specifier as defined in PEP 440](https://peps.python.org/pep-0440/#version-specifiers). Here are some examples:\n\n| `requires-python`       | Meaning                                  |\n| ----------------------- | ---------------------------------------- |\n| `>=3.7`                 | Python 3.7 and above                     |\n| `>=3.7,<3.11`           | Python 3.7, 3.8, 3.9 and 3.10            |\n| `>=3.6,!=3.8.*,!=3.9.*` | Python 3.6 and above, except 3.8 and 3.9 |\n\n## Working with older Python versions\n\n--- 2.21.0\n\n    PDM now supports 3.9 and above as the python version of projects.\n\nAlthough PDM run on Python 3.9 and above, you can still have lower Python versions for your **working project**. But remember, if your project is a library, which needs to be built, published or installed, you make sure the PEP 517 build backend being used supports the lowest Python version you need. For instance, the default backend `pdm-backend` only works on Python 3.7+, so if you run [`pdm build`](../reference/cli.md#build) on a project with Python 3.6, you will get an error. Most modern build backends have dropped the support for Python 3.6 and lower, so it is highly recommended to upgrade the Python version to 3.7+. Here are the supported Python range for some commonly used build backends, we only list those that support PEP 621 since otherwise PDM can't work with them.\n\n| Backend               | Supported Python | Support PEP 621 |\n| --------------------- | ---------------- | --------------- |\n| `pdm-backend`         | `>=3.7`          | Yes             |\n| `setuptools>=60`      | `>=3.7`          | Experimental    |\n| `hatchling`           | `>=3.7`          | Yes             |\n| `flit-core>=3.4`      | `>=3.6`          | Yes             |\n| `flit-core>=3.2,<3.4` | `>=3.4`          | Yes             |\n\nNote that if your project is an application (i.e. without the `name` metadata),\nthe above limitation of backends does not apply. Therefore, if you don't need a build backend you can use any Python version `>=2.7`.\n\n## Import the project from other package managers\n\nIf you are already using other package manager tools like Pipenv or Poetry, it is easy to migrate to PDM.\nPDM provides `import` command so that you don't have to initialize the project manually, it now supports:\n\n1. Pipenv's `Pipfile`\n2. Poetry's section in `pyproject.toml`\n3. Flit's section in `pyproject.toml`\n4. `requirements.txt` format used by pip\n5. setuptools `setup.py`(It requires `setuptools` to be installed in the project environment. You can do this by configuring `venv.with_pip` to `true` for venv and `pdm add setuptools` for `__pypackages__`)\n\nAlso, when you are executing [`pdm init`](../reference/cli.md#init) or [`pdm install`](../reference/cli.md#install), PDM can auto-detect possible files to import if your PDM project has not been initialized yet.\n\n!!! info\n    Converting a `setup.py` will execute the file with the project interpreter. Make sure `setuptools` is installed with the interpreter and the `setup.py` is trusted.\n\n## Working with version control\n\nYou **must** commit the `pyproject.toml` file. You **should** commit the `pdm.lock` and `pdm.toml` file. **Do not** commit the `.pdm-python` file.\n\nThe `pyproject.toml` file must be committed as it contains the project's build metadata and dependencies needed for PDM.\nIt is also commonly used by other python tools for configuration. Read more about the `pyproject.toml` file at\n[Pip documentation](https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/).\n\nYou should be committing the `pdm.lock` file, by doing so you ensure that all installers are using the same versions of dependencies.\nTo learn how to update dependencies see [update existing dependencies](./dependency.md#update-existing-dependencies).\n\n`pdm.toml` contains some project-wide configuration and it may be useful to commit it for sharing.\n\n`.pdm-python` stores the **Python path** used by the **current** project and doesn't need to be shared.\n\n## Show the current Python environment\n\n```bash\n$ pdm info\nPDM version:\n  2.0.0\nPython Interpreter:\n  /opt/homebrew/opt/python@3.9/bin/python3.9 (3.9)\nProject Root:\n  /Users/fming/wkspace/github/test-pdm\nProject Packages:\n  /Users/fming/wkspace/github/test-pdm/__pypackages__/3.9\n\n# Show environment info\n$ pdm info --env\n{\n  \"implementation_name\": \"cpython\",\n  \"implementation_version\": \"3.9.0\",\n  \"os_name\": \"nt\",\n  \"platform_machine\": \"AMD64\",\n  \"platform_release\": \"10\",\n  \"platform_system\": \"Windows\",\n  \"platform_version\": \"10.0.18362\",\n  \"python_full_version\": \"3.9.0\",\n  \"platform_python_implementation\": \"CPython\",\n  \"python_version\": \"3.9\",\n  \"sys_platform\": \"win32\"\n}\n```\n\n[This command](../reference/cli.md#info) is useful for checking which mode is being used by the project:\n\n- If **Project Packages** is `None`, [virtualenv mode](./venv.md) is enabled.\n- Otherwise, [PEP 582 mode](./pep582.md) is enabled.\n\nNow, you have set up a new PDM project and get a `pyproject.toml` file. Refer to [metadata section](../reference/pep621.md)\nabout how to write `pyproject.toml` properly.\n"
  },
  {
    "path": "docs/usage/publish.md",
    "content": "# Build and Publish\n\nIf you are developing a library, after adding dependencies to your project, and finishing the coding, it's time to build and publish your package. It is as simple as one command:\n\n```bash\npdm publish\n```\n\nThis will automatically build a wheel and a source distribution(sdist), and upload them to the PyPI index.\n\nPyPI requires API tokens to publish packages, you can use `__token__` as the username and API token as the password.\n\nTo specify another repository other than PyPI, use the `--repository` option, the parameter can be either the upload URL or the name of the repository stored in the config file.\n\n```bash\npdm publish --repository testpypi\npdm publish --repository https://test.pypi.org/legacy/\n```\n\n## Publish with trusted publishers\n\nYou can configure trusted publishers for PyPI so that you don't need to expose the PyPI tokens in the release workflow. To do this, follow\n[the guide](https://docs.pypi.org/trusted-publishers/adding-a-publisher/) to add a publisher write a action as below:\n\n### GitHub Actions\n\n```yaml\non:\n  release:\n    types: [published]\n\njobs:\n  pypi-publish:\n    name: upload release to PyPI\n    runs-on: ubuntu-latest\n    permissions:\n      # This permission is needed for private repositories.\n      contents: read\n      # IMPORTANT: this permission is mandatory for trusted publishing\n      id-token: write\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: pdm-project/setup-pdm@v4\n\n      - name: Publish package distributions to PyPI\n        run: pdm publish\n```\n\n### GitLab CI\n\n```yaml\nimage: python:3.12-bookworm\nbefore_script:\n  - pip install pdm\n\npublish-package:\n  stage: release\n  environment: production\n  id_tokens:\n    PYPI_ID_TOKEN: # for testpypi: TESTPYPI_ID_TOKEN\n      aud: \"pypi\" # testpypi\n  script:\n    - pdm publish\n```\n\n## Build and publish separately\n\nYou can also build the package and upload it in two steps, to allow you to inspect the built artifacts before uploading.\n\n```bash\npdm build\n```\n\nThere are many options to control the build process, depending on the backend used. Refer to the [build configuration](../reference/build.md) section for more details.\n\nThe artifacts will be created at `dist/` and able to upload to PyPI.\n\n```bash\npdm publish --no-build\n```\n"
  },
  {
    "path": "docs/usage/scripts.md",
    "content": "# PDM Scripts\n\nLike `npm run`, with PDM, you can run arbitrary scripts or commands with local packages loaded.\n\n## Arbitrary Scripts\n\n```bash\npdm run flask run -p 54321\n```\n\nIt will run `flask run -p 54321` in the environment that is aware of packages in your project environment.\n\n## Single-file Scripts\n\n+++ 2.16.0\n\nPDM can run single-file scripts with [inline script metadata](https://peps.python.org/pep-0723/) specified by PEP 723.\n\nThe following is an example of a script with embedded metadata:\n\n```python\n# test_script.py\n# /// script\n# requires-python = \">=3.11\"\n# dependencies = [\n#   \"requests<3\",\n#   \"rich\",\n# ]\n# ///\n\nimport requests\nfrom rich.pretty import pprint\n\nresp = requests.get(\"https://peps.python.org/api/peps.json\")\ndata = resp.json()\npprint([(k, v[\"title\"]) for k, v in data.items()][:10])\n```\n\nWhen you run it with `pdm run test_script.py`, PDM will create a temporary environment with the specified dependencies installed and run the script:\n\n```python\n[\n│   ('1', 'PEP Purpose and Guidelines'),\n│   ('2', 'Procedure for Adding New Modules'),\n│   ('3', 'Guidelines for Handling Bug Reports'),\n│   ('4', 'Deprecation of Standard Modules'),\n│   ('5', 'Guidelines for Language Evolution'),\n│   ('6', 'Bug Fix Releases'),\n│   ('7', 'Style Guide for C Code'),\n│   ('8', 'Style Guide for Python Code'),\n│   ('9', 'Sample Plaintext PEP Template'),\n│   ('10', 'Voting Guidelines')\n]\n```\nAdd `--reuse-env` option if you want to reuse the environment created last time.\nYou can also add `[tool.pdm]` section to the script metadata to configure PDM. For example:\n\n```python\n# test_script.py\n# /// script\n# requires-python = \">=3.11\"\n# dependencies = [\n#   \"requests<3\",\n#   \"rich\",\n# ]\n#\n# [[tool.pdm.source]]  # Use a custom index\n# url = \"https://mypypi.org/simple\"\n# name = \"pypi\"\n# ///\n```\n\nRead the [specification](https://packaging.python.org/en/latest/specifications/inline-script-metadata/#inline-script-metadata) for more details.\n\n## User Scripts\n\nPDM also supports custom script shortcuts in the optional `[tool.pdm.scripts]` section of `pyproject.toml`.\n\n!!! note \"Confuse with `[project.scripts]`?\"\n    There is another field `[project.scripts]` in `pyproject.toml`, and the scripts can also be invoked with `pdm run`. It's used to define the console script entry points to be installed with the package. Therefore, the executables can only be run after the project itself is installed into the environment. That is to say, you must have `distribution = true`.\n\n    In contrast, `[tool.pdm.scripts]` defines some tasks to be run in your project. It works for projects regardless of whether the `distribution` is `true` or `false`. The tasks are primarily for development and testing purposes and support more types and settings, as will be shown later., you can regard it as a replacement for `Makefile`. It doesn't require the project to be installed but requires the existence of a `pyproject.toml` file.\n\n    See more explanations about `[project.scripts]` [here](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#creating-executable-scripts).\n\nYou can then run `pdm run <script_name>` to invoke the script in the context of your PDM project. For example:\n\n```toml\n[tool.pdm.scripts]\nstart = \"flask run -p 54321\"\n```\n\nAnd then in your terminal:\n\n```bash\n$ pdm run start\nFlask server started at http://127.0.0.1:54321\n```\n\nAny following arguments will be appended to the command:\n\n```bash\n$ pdm run start -h 0.0.0.0\nFlask server started at http://0.0.0.0:54321\n```\n\n!!! note \"Yarn-like script shortcuts\"\n    There is a builtin shortcut making all scripts available as root commands as long as the script does not conflict with any builtin or plugin-contributed command.\n    Said otherwise, if you have a `start` script, you can run both `pdm run start` and `pdm start`.\n    But if you have an `install` script, only `pdm run install` will run it, `pdm install` will still run the builtin `install` command.\n\nPDM supports 4 types of scripts:\n\n### `cmd`\n\nPlain text scripts are regarded as normal command, or you can explicitly specify it:\n\n```toml\n[tool.pdm.scripts]\nstart = {cmd = \"flask run -p 54321\"}\n```\n\nIn some cases, such as when wanting to add comments between parameters, it might be more convenient.\nto specify the command as an array instead of a string:\n\n```toml\n[tool.pdm.scripts]\nstart = {cmd = [\n    \"flask\",\n    \"run\",\n    # Important comment here about always using port 54321\n    \"-p\", \"54321\"\n]}\n```\n\n### `shell`\n\nShell scripts can be used to run more shell-specific tasks, such as pipeline and output redirecting.\nThis is basically run via `subprocess.Popen()` with `shell=True`:\n\n```toml\n[tool.pdm.scripts]\nfilter_error = {shell = \"cat error.log|grep CRITICAL > critical.log\"}\n```\n\n### `call`\n\nThe script can be also defined as calling a python function in the form `<module_name>:<func_name>`:\n\n```toml\n[tool.pdm.scripts]\nfoobar = {call = \"foo_package.bar_module:main\"}\n```\n\nThe function can be supplied with literal arguments:\n\n```toml\n[tool.pdm.scripts]\nfoobar = {call = \"foo_package.bar_module:main('dev')\"}\n```\n\n### `composite`\n\nThis script kind execute other defined scripts:\n\n```toml\n[tool.pdm.scripts]\nlint = \"flake8\"\ntest = \"pytest\"\nall = {composite = [\"lint\", \"test\"]}\n```\n\nRunning `pdm run all` will run `lint` first and then `test` if `lint` succeeded.\n\n+++ 2.13.0\n\nTo override the default behavior and continue the execution of the remaining scripts after a failure,\nset the `keep_going` option to `true`:\n\n```toml\n[tool.pdm.scripts]\nlint = \"flake8\"\ntest = \"pytest\"\nall.composite = [\"lint\", \"test\"]\nall.keep_going = true\n```\n\nIf `keep_going` set to `true` return code of composite script is either '0' if all succeeded or the code of last failed individual script.\n\nYou can also provide arguments to the called scripts:\n\n```toml\n[tool.pdm.scripts]\nlint = \"flake8\"\ntest = \"pytest\"\nall = {composite = [\"lint mypackage/\", \"test -v tests/\"]}\n```\n\n!!! note\n    Argument passed on the command line are given to each called task.\n\nYou can also use the `composite` script to combine multiple commands:\n\n```toml\n[tool.pdm.scripts]\nmytask.composite = [\n    \"echo 'Hello'\",\n    \"echo 'World'\"\n]\n```\n\n## Script Options\n\n### `env`\n\nAll environment variables set in the current shell can be seen by `pdm run` and will be expanded when executed.\nBesides, you can also define some fixed environment variables in your `pyproject.toml`:\n\n```toml\n[tool.pdm.scripts]\nstart.cmd = \"flask run -p 54321\"\nstart.env = {FOO = \"bar\", FLASK_DEBUG = \"1\"}\n```\n\nNote how we use [TOML's syntax](https://github.com/toml-lang/toml) to define a composite dictionary.\n\n!!! note \"About environment variable substitution\"\n    Variables in script specifications can be substituted in all script types. In `cmd` scripts, only `${VAR}`\n    syntax is supported on all platforms, however in `shell` scripts, the syntax is platform-dependent. For example,\n    Windows cmd uses `%VAR%` while bash uses `$VAR`.\n\n!!! note\n    Environment variables specified on a composite task level will override those defined by called tasks.\n\n### `env_file`\n\nYou can also store all environment variables in a dotenv file and let PDM read it:\n\n```toml\n[tool.pdm.scripts]\nstart.cmd = \"flask run -p 54321\"\nstart.env_file = \".env\"\n```\n\nThe variables within the dotenv file will *not* override any existing environment variables.\nIf you want the dotenv file to override existing environment variables use the following:\n\n```toml\n[tool.pdm.scripts]\nstart.cmd = \"flask run -p 54321\"\nstart.env_file.override = \".env\"\n```\n\n!!! note \"Environment variable loading order\"\n    Env vars loaded from different sources are loaded in the following order:\n\n    1. OS environment variables\n    2. Project environments such as `PDM_PROJECT_ROOT`, `PATH`, `VIRTUAL_ENV`, etc\n    3. Dotenv file specified by `env_file`\n    4. Env vars mapping specified by `env`\n\n    Env vars from the latter sources will override those from the former sources.\n    A dotenv file specified on a composite task level will override those defined by called tasks.\n\n    An env var can contain a reference to another env var from the sources loaded before, for example:\n\n    ```\n    VAR=42\n    FOO=hello-${VAR}\n    ```\n    will result in `FOO=hello-42`. The reference can also contain a default value with the syntax `${VAR:-default}`.\n\n### `working_dir`\n\n+++ 2.13.0\n\nYou can set the current working directory for the script:\n\n```toml\n[tool.pdm.scripts]\nstart.cmd = \"flask run -p 54321\"\nstart.working_dir = \"subdir\"\n```\n\nRelative paths are resolved against the project root.\n\n+++ 2.20.2\n\nTo identify the original calling working directory, each script gets the environment variable `PDM_RUN_CWD` injected.\n\n### `site_packages`\n\nTo make sure the running environment is properly isolated from the outer Python interpreter,\nsite-packages from the selected interpreter WON'T be loaded into `sys.path`, unless any of the following conditions holds:\n\n1. The executable is from `PATH` but not inside the `__pypackages__` folder.\n2. `-s/--site-packages` flag is following `pdm run`.\n3. `site_packages = true` is in either the script table or the global setting key `_`.\n\nNote that site-packages will always be loaded if running with PEP 582 enabled(without the `pdm run` prefix).\n\n### Shared Options\n\nIf you want the options to be shared by all tasks run by `pdm run`,\nyou can write them under a special key `_` in `[tool.pdm.scripts]` table:\n\n```toml\n[tool.pdm.scripts]\n_.env_file = \".env\"\nstart = \"flask run -p 54321\"\nmigrate_db = \"flask db upgrade\"\n```\n\nBesides, inside the tasks, `PDM_PROJECT_ROOT` environment variable will be set to the project root.\n\n### Arguments placeholder\n\nBy default, all user provided extra arguments are simply appended to the command (or to all the commands for `composite` tasks).\n\nIf you want more control over the user provided extra arguments, you can use the `{args}` placeholder.\nIt is available for all script types and will be interpolated properly for each:\n\n```toml\n[tool.pdm.scripts]\ncmd = \"echo '--before {args} --after'\"\nshell = {shell = \"echo '--before {args} --after'\"}\ncomposite = {composite = [\"cmd --something\", \"shell {args}\"]}\n```\n\nwill produce the following interpolations (those are not real scripts, just here to illustrate the interpolation):\n\n```shell\n$ pdm run cmd --user --provided\n--before --user --provided --after\n$ pdm run cmd\n--before --after\n$ pdm run shell --user --provided\n--before --user --provided --after\n$ pdm run shell\n--before --after\n$ pdm run composite --user --provided\ncmd --something\nshell --before --user --provided --after\n$ pdm run composite\ncmd --something\nshell --before --after\n```\n\nYou may optionally provide default values that will be used if no user arguments are provided:\n\n```toml\n[tool.pdm.scripts]\ntest = \"echo '--before {args:--default --value} --after'\"\n```\n\nwill produce the following:\n\n```shell\n$ pdm run test --user --provided\n--before --user --provided --after\n$ pdm run test\n--before --default --value --after\n```\n\n!!! note\n    As soon a placeholder is detected, arguments are not appended anymore.\n    This is important for `composite` scripts because if a placeholder\n    is detected on one of the subtasks, none for the subtasks will have\n    the arguments appended, you need to explicitly pass the placeholder\n    to every nested command requiring it.\n\n!!! note\n    `call` scripts don't support the `{args}` placeholder as they have\n    access to `sys.argv` directly to handle such complex cases and more.\n\n### `{pdm}` placeholder\n\nSometimes you may have multiple PDM installations, or `pdm` installed with a different name. This\ncould for example occur in a CI/CD situation, or when working with different PDM versions in\ndifferent repos. To make your scripts more robust you can use `{pdm}` to use the PDM entrypoint\nexecuting the script. This will expand to `{sys.executable} -m pdm`.\n\n```toml\n[tool.pdm.scripts]\nwhoami = { shell = \"echo `{pdm} -V` was called as '{pdm} -V'\" }\n```\n\nwill produce the following output:\n\n```shell\n$ pdm whoami\nPDM, version 0.1.dev2501+g73651b7.d20231115 was called as /usr/bin/python3 -m pdm -V\n\n$ pdm2.8 whoami\nPDM, version 2.8.0 was called as <snip>/venvs/pdm2-8/bin/python -m pdm -V\n```\n\n!!! note\n    While the above example uses PDM 2.8, this functionality was introduced in the 2.10 series and only backported for the showcase.\n\n## Show the List of Scripts\n\nUse `pdm run --list/-l` to show the list of available script shortcuts:\n\n```bash\n$ pdm run --list\n╭─────────────┬───────┬───────────────────────────╮\n│ Name        │ Type  │ Description               │\n├─────────────┼───────┼───────────────────────────┤\n│ test_cmd    │ cmd   │ flask db upgrade          │\n│ test_script │ call  │ call a python function    │\n│ test_shell  │ shell │ shell command             │\n╰─────────────┴───────┴───────────────────────────╯\n```\n\nYou can add an `help` option with the description of the script, and it will be displayed in the `Description` column in the above output.\n\n!!! note\n    Tasks with a name starting with an underscore (`_`) are considered internal (helpers...) and are not shown in the listing.\n\n## Pre & Post Scripts\n\nLike `npm`, PDM also supports tasks composition by pre and post scripts, pre script will be run before the given task and post script will be run after.\n\n```toml\n[tool.pdm.scripts]\npre_compress = \"{{ Run BEFORE the `compress` script }}\"\ncompress = \"tar czvf compressed.tar.gz data/\"\npost_compress = \"{{ Run AFTER the `compress` script }}\"\n```\n\nIn this example, `pdm run compress` will run all these 3 scripts sequentially.\n\n!!! note \"The pipeline fails fast\"\n    In a pipeline of pre - self - post scripts, a failure will cancel the subsequent execution.\n\n## Hook Scripts\n\nUnder certain situations PDM will look for some special hook scripts for execution:\n\n- `post_init`: Run after `pdm init`\n- `pre_install`: Run before installing packages\n- `post_install`: Run after packages are installed\n- `pre_lock`: Run before dependency resolution\n- `post_lock`: Run after dependency resolution\n- `pre_build`: Run before building distributions\n- `post_build`: Run after distributions are built\n- `pre_publish`: Run before publishing distributions\n- `post_publish`: Run after distributions are published\n- `pre_script`: Run before any script\n- `post_script`: Run after any script\n- `pre_run`: Run once before run script invocation\n- `post_run`: Run once after run script invocation\n\n!!! note\n    Pre & post scripts can't receive any arguments.\n\n!!! note \"Avoid name conflicts\"\n    If there exists an `install` scripts under `[tool.pdm.scripts]` table, `pre_install`\n    scripts can be triggered by both `pdm install` and `pdm run install`. So it is\n    recommended to not use the preserved names.\n\n!!! note\n    Composite tasks can also have pre and post scripts.\n    Called tasks will run their own pre and post scripts.\n\n## Skipping scripts\n\nBecause, sometimes it is desirable to run a script but without its hooks or pre and post scripts,\nthere is a `--skip=:all` which will disable all hooks, pre and post.\nThere is also `--skip=:pre` and `--skip=:post` allowing to respectively\nskip all `pre_*` hooks and all `post_*` hooks.\n\nIt is also possible to need a pre script but not the post one,\nor to need all tasks from a composite tasks except one.\nFor those use cases, there is a finer grained `--skip` parameter\naccepting a list of tasks or hooks name to exclude.\n\n```bash\npdm run --skip pre_task1,task2 my-composite\n```\n\nThis command will run the `my-composite` task and skip the `pre_task1` hook as well as the `task2` and its hooks.\n\nYou can also provide you skip list in `PDM_SKIP_HOOKS` environment variable\nbut it will be overridden as soon as the `--skip` parameter is provided.\n\nThere is more details on hooks and pre/post scripts behavior on [the dedicated hooks page](hooks.md).\n"
  },
  {
    "path": "docs/usage/template.md",
    "content": "# Create Project From a Template\n\nSimilar to `yarn create` and `npm create`, PDM also supports initializing or creating a project from a template.\nThe template is given as a positional argument of `pdm new`, in one of the following forms:\n\n- `pdm new django my-project` - Create a new project `my-project` from the template `https://github.com/pdm-project/template-django`\n- `pdm new https://github.com/frostming/pdm-template-django my-project` - Initialize the project from a Git URL. Both HTTPS and SSH URL are acceptable.\n- `pdm new django@v2 my-project` - To check out the specific branch or tag. Full Git URL also supports it.\n- `pdm new /path/to/template my-project` - Initialize the project from a template directory on local filesystem.\n- `pdm new minimal my-project` - Initialize with the builtin \"minimal\" template, that only generates a `pyproject.toml`.\n\nAnd `pdm new my-project` will use the default template built in and create a project at the given path.\n\n`pdm init` command also supports the same template argument. The project will be initialized at the current directory, existing files with the same name will be overwritten.\n\n## Contribute a template\n\nAccording to the first form of the template argument, `pdm init <name>` will refer to the template repository located at `https://github.com/pdm-project/template-<name>`. To contribute a template, you can create a template repository and establish a request to transfer the\nownership to `pdm-project` organization(it can be found at the bottom of the repository settings page). The administrators of the organization will review the request and complete the subsequent steps. You will be added as the repository maintainer if the transfer is accepted.\n\n## Requirements for a template\n\nA template repository must be a pyproject-based project, which contains a `pyproject.toml` file with PEP-621 compliant metadata.\nNo other special config files are required.\n\n## Project name replacement\n\nOn initialization, the project name in the template will be replaced by the name of the new project. This is done by a recursive full-text search and replace. The import name, which is derived from the project name by replacing all non-alphanumeric characters with underscores and lowercasing, will also be replaced in the same way.\n\nFor example, if the project name is `foo-project` in the template and you want to initialize a new project named `bar-project`, the following replacements will be made:\n\n- `foo-project` -> `bar-project` in all `.md` files and `.rst` files\n- `foo_project` -> `bar_project` in all `.py` files\n- `foo_project` -> `bar_project` in the directory name\n- `foo_project.py` -> `bar_project.py` in the file name\n\nTherefore, we don't support name replacement if the import name isn't derived from the project name.\n\n## Use other project generators\n\nIf you are seeking for a more powerful project generator, you can use [cookiecutter](https://github.com/cookiecutter/cookiecutter) via `--cookiecutter` option and [copier](https://github.com/copier-org/copier) via `--copier` option.\n\nYou need to install `cookiecutter` and `copier` respectively to use them. You can do this by running `pdm self add <package>`.\nTo use them:\n\n```bash\npdm init --cookiecutter gh:cjolowicz/cookiecutter-hypermodern-python\n# or\npdm init --copier gh:pawamoy/copier-pdm --UNSAFE\n```\n"
  },
  {
    "path": "docs/usage/uv.md",
    "content": "# Use uv (Experimental)\n\n+++ 2.19.0\n\nPDM has experimental support for [uv](https://github.com/astral-sh/uv) as the resolver and installer. To enable it:\n\n```\npdm config use_uv true\n```\n\nPDM will automatically detect the `uv` binary on your system. You need to install `uv` first. See [uv's installation guide](https://docs.astral.sh/uv/getting-started/installation/) for more details.\n\n## Reuse the Python installations of uv\n\nuv also supports installing Python interpreters. To avoid overhead, you can configure PDM to reuse the Python installations of uv by:\n\n```\npdm config python.install_root $(uv python dir --color never)\n```\n\n## Limitations\n\nDespite the significant performance improvements brought by uv, it is important to note the following limitations:\n\n- The cache files are stored in uv's own cache directory, and you have to use `uv` command to manage them.\n- PEP 582 local packages layout is not supported.\n- `inherit_metadata` lock strategy is not supported by uv. This will be ignored when writing to the lock file.\n- Update strategies other than `all` and `reuse` are not supported.\n- Editable requirement must be a local path. Requirements like `-e git+<git_url>` are not supported.\n- `excludes` settings under `[tool.pdm.resolution]` are not supported.\n- Cross-platform lock targets are not needed by uv resolver, uv always generates universal lock files.\n- `include_packages` and `exclude_packages` settings under `[tool.pdm.source]` are not supported.\n"
  },
  {
    "path": "docs/usage/venv.md",
    "content": "# Working with Virtual Environments\n\nWhen you run [`pdm init`](../reference/cli.md#init) command, PDM will [ask for the Python interpreter to use](./project.md#choose-a-python-interpreter) in the project, which is the base interpreter to install dependencies and run tasks.\n\nCompared to [PEP 582](https://www.python.org/dev/peps/pep-0582/), virtual environments are considered more mature and have better support in the Python ecosystem as well as IDEs. Therefore, virtualenv is the default mode if not configured otherwise.\n\n!!! note \"Configure pdm to use virtual environment or PEP 582\"\n    By default pdm is configured to use virtual environment instead of PEP 582. But this behavior can be changed with `pdm config python.use_venv False` config variable.\n\n**Virtual environments will be used if the project interpreter (the interpreter stored in `.pdm-python`, which can be checked by `pdm info`) is from a virtualenv.**\n\n## Virtualenv auto-creation\n\nBy default, PDM prefers to use the virtualenv layout as other package managers do. When you run `pdm install` the first time on a new PDM-managed project, whose Python interpreter is not decided yet, PDM will create a virtualenv in `<project_root>/.venv`, and install dependencies into it. In the interactive session of `pdm init`, PDM will also ask to create a virtualenv for you.\n\nYou can choose the backend used by PDM to create a virtualenv. Currently it supports three backends:\n\n- [`virtualenv`](https://virtualenv.pypa.io/)(default)\n- `venv`\n- `conda`\n\nYou can change it by `pdm config venv.backend [virtualenv|venv|conda]`.\n\n+++ 2.13.0\n\n    Moreover, when `python.use_venv` config is set to `true`, PDM will always try to create a virtualenv when using `pdm use` to switch the Python interpreter.\n\n## Create a virtualenv yourself\n\nYou can create more than one virtualenvs with whatever Python version you want.\n\n```bash\n# Create a virtualenv based on 3.9 interpreter\npdm venv create 3.9\n# Assign a different name other than the version string\npdm venv create --name for-test 3.9\n# Use venv as the backend to create, support 3 backends: virtualenv(default), venv, conda\npdm venv create --with venv 3.10\n```\n\n## The location of virtualenvs\n\nIf no `--name` is given, PDM will create the venv in `<project_root>/.venv`. Otherwise, virtualenvs go to the location specified by the `venv.location` configuration.\nThey are named as `<project_name>-<path_hash>-<name_or_python_version>` to avoid name collision.\nYou can disable the in-project virtualenv creation by `pdm config venv.in_project false`. And all virtualenvs will be created under `venv.location`.\n\n## Reuse the virtualenv you created elsewhere\n\nYou can tell PDM to use a virtualenv you created in preceding steps, with [`pdm use`](../reference/cli.md#use):\n\n```bash\npdm use -f /path/to/venv\n```\n\n## Virtualenv auto-detection\n\nWhen no interpreter is stored in the project config or `PDM_IGNORE_SAVED_PYTHON` env var is set, PDM will try to detect possible virtualenvs to use:\n\n- `venv`, `env`, `.venv` directories in the project root\n- The currently activated virtualenv, unless `PDM_IGNORE_ACTIVE_VENV` is set\n\n## List all virtualenvs created with this project\n\n```bash\n$ pdm venv list\nVirtualenvs created with this project:\n\n-  3.8.6: C:\\Users\\Frost Ming\\AppData\\Local\\pdm\\pdm\\venvs\\test-project-8Sgn_62n-3.8.6\n-  for-test: C:\\Users\\Frost Ming\\AppData\\Local\\pdm\\pdm\\venvs\\test-project-8Sgn_62n-for-test\n-  3.9.1: C:\\Users\\Frost Ming\\AppData\\Local\\pdm\\pdm\\venvs\\test-project-8Sgn_62n-3.9.1\n```\n\n## Show the path or python interpreter of a virtualenv\n\n```bash\npdm venv --path for-test\npdm venv --python for-test\n```\n\n## Remove a virtualenv\n\n```bash\n$ pdm venv remove for-test\nVirtualenvs created with this project:\nWill remove: C:\\Users\\Frost Ming\\AppData\\Local\\pdm\\pdm\\venvs\\test-project-8Sgn_62n-for-test, continue? [y/N]:y\nRemoved C:\\Users\\Frost Ming\\AppData\\Local\\pdm\\pdm\\venvs\\test-project-8Sgn_62n-for-test\n```\n\n## Activate a virtualenv\n\nInstead of spawning a subshell like what `pipenv` and `poetry` do, `pdm venv` doesn't create the shell for you but print the activate command to the console. In this way you won't leave the current shell. You can then feed the output to `eval` to activate the virtualenv:\n\n=== \"bash/csh/zsh\"\n\n    ```bash\n    $ eval $(pdm venv activate for-test)\n    (test-project-for-test) $  # Virtualenv entered\n    ```\n\n=== \"Fish\"\n\n    ```bash\n    $ eval (pdm venv activate for-test)\n    ```\n\n=== \"Powershell\"\n\n    ```ps1\n    PS1> Invoke-Expression (pdm venv activate for-test)\n    ```\n\n    Additionally, if the project interpreter is a venv Python, you can omit the name argument following activate.\n\n!!! note\n    `venv activate` **does not** switch the Python interpreter used by the project. It only changes the shell by injecting the virtualenv paths to environment variables. For the aforementioned purpose, use the `pdm use` command.\n\nFor more CLI usage, see the [`pdm venv`](../reference/cli.md#venv) documentation.\n\n!!! tip \"Looking for `pdm shell`?\"\n    PDM doesn't provide a `shell` command because many fancy shell functions may not work perfectly in a subshell, which brings a maintenance burden to support all the corner cases. However, you can still gain the ability via the following ways:\n\n    - Use `pdm run $SHELL`, this will spawn a subshell with the environment variables set properly. **The subshell can be quit with `exit` or `Ctrl+D`.**\n    - Add a shell function to activate the virtualenv, here is an example of BASH function that also works on ZSH:\n\n      ```bash\n      pdm() {\n        local command=$1\n\n        if [[ \"$command\" == \"shell\" ]]; then\n            eval $(pdm venv activate)\n        else\n            command pdm $@\n        fi\n      }\n      ```\n\n      Copy and paste this function to your `~/.bashrc` file and restart your shell.\n\n      For `fish` shell you can put the following into your `~/fish/config.fish` or in `~/.config/fish/config.fish`\n\n      ```fish\n        function pdm\n            set cmd $argv[1]\n\n            if test \"$cmd\" = \"shell\"\n                eval (pdm venv activate)\n            else\n                command pdm $argv\n            end\n        end\n      ```\n\n      Now you can run `pdm shell` to activate the virtualenv.\n      **The virtualenv can be deactivated with `deactivate` command as usual.**\n\n## Prompt customization\n\nBy default when you activate a virtualenv, the prompt will show: `{project_name}-{python_version}`.\n\nFor example if your project is named `test-project`:\n\n```bash\n$ eval $(pdm venv activate for-test)\n(test-project-3.10) $  # {project_name} == test-project and {python_version} == 3.10\n```\n\nThe format can be customized before virtualenv creation with the [`venv.prompt`](../reference/configuration.md) configuration or `PDM_VENV_PROMPT` environment variable (before a `pdm init` or `pdm venv create`).\nAvailable variables are:\n\n- `project_name`: name of your project\n- `python_version`: version of Python (used by the virtualenv)\n\n```bash\n$ PDM_VENV_PROMPT='{project_name}-py{python_version}' pdm venv create --name test-prompt\n$ eval $(pdm venv activate test-prompt)\n(test-project-py3.10) $\n```\n\n## Run a command in a virtual environment without activating it\n\n```bash\n# Run a script\npdm run --venv test test\n# Install packages\npdm sync --venv test\n# List the packages installed\npdm list --venv test\n```\n\nThere are other commands supporting `--venv` flag or `PDM_IN_VENV` environment variable, see the [CLI reference](../reference/cli.md). You should create the virtualenv with `pdm venv create --name <name>` before using this feature.\n\n## Switch to a virtualenv as the project environment\n\nBy default, if you use `pdm use` and select a non-venv Python, the project will be switched to [PEP 582 mode](./pep582.md). We also allow you to switch to a named virtual environment via the `--venv` flag:\n\n```bash\n# Switch to a virtualenv named test\n$ pdm use --venv test\n# Switch to the in-project venv located at $PROJECT_ROOT/.venv\n$ pdm use --venv in-project\n```\n\n## Disable virtualenv mode\n\nYou can disable the auto-creation and auto-detection for virtualenv by `pdm config python.use_venv false`.\n**If venv is disabled, PEP 582 mode will always be used even if the selected interpreter is from a virtualenv.**\n\n## Including pip in your virtual environment\n\nBy default PDM will not include `pip` in virtual environments.\nThis increases isolation by ensuring that _only your dependencies_ are installed in the virtual environment.\n\nTo install `pip` once (if for example you want to install arbitrary dependencies in CI) you can run:\n\n```bash\n# Install pip in the virtual environment\npdm run python -m ensurepip\n# Install arbitrary dependencies\n# These dependencies are not checked for conflicts against lockfile dependencies!\npdm run python -m pip install coverage\n```\n\nOr you can create the virtual environment with `--with-pip`:\n\n```bash\npdm venv create --with-pip 3.9\n```\n\nSee the [ensurepip docs](https://docs.python.org/3/library/ensurepip.html) for more details on `ensurepip`.\n\nIf you want to permanently configure PDM to include `pip` in virtual environments you can use the [`venv.with_pip`](../reference/configuration.md) configuration.\n"
  },
  {
    "path": "install-pdm.ps1",
    "content": "#!/usr/bin/env pwsh\n<#\n.SYNOPSIS\n    PDM Installer Script for Windows\n    Downloads and installs PDM from GitHub released binaries\n\n.DESCRIPTION\n    This script downloads PDM binaries from GitHub releases, verifies checksums,\n    and installs PDM on Windows systems.\n\n.PARAMETER Version\n    Specify the version to be installed (default: latest)\n\n.PARAMETER InstallPath\n    Specify the installation directory (default: $env:LOCALAPPDATA\\Programs\\pdm)\n\n.PARAMETER SkipAddToPath\n    Do not add binary to the PATH\n\n.PARAMETER SkipChecksum\n    Skip checksum verification\n\n.EXAMPLE\n    .\\install-pdm.ps1\n    Install latest version of PDM\n\n.EXAMPLE\n    .\\install-pdm.ps1 -Version \"2.26.1\"\n    Install specific version of PDM\n\n.EXAMPLE\n    .\\install-pdm.ps1 -InstallPath \"C:\\Tools\\pdm\"\n    Install to custom location\n\n.EXAMPLE\n    .\\install-pdm.ps1 -SkipChecksum\n    Skip checksum verification\n#>\n\n[CmdletBinding()]\nparam(\n    [string]$Version = $env:PDM_VERSION,\n    [string]$InstallPath = $env:PDM_HOME,\n    [switch]$SkipAddToPath = [bool]$env:PDM_SKIP_ADD_TO_PATH,\n    [switch]$SkipChecksum = [bool]$env:PDM_SKIP_CHECKSUM\n)\n\n# Set strict mode\nSet-StrictMode -Version Latest\n$ErrorActionPreference = 'Stop'\n\n# Configuration\n$Repo = if ($env:PDM_REPO) { $env:PDM_REPO } else { \"pdm-project/pdm\" }\n$DefaultInstallPath = \"$env:LOCALAPPDATA\\Programs\\pdm\"\n\n# Color output - Use Windows-friendly approach\nfunction Write-ColorOutput {\n    param(\n        [string]$Text,\n        [string]$Color = \"Cyan\",\n        [switch]$Bold\n    )\n\n    # Use PSStyle if available (PowerShell 7.2+)\n    if (Get-Variable -Name PSStyle -ErrorAction SilentlyContinue) {\n        $colorCode = switch ($Color) {\n            \"Green\" { $PSStyle.Foreground.Green }\n            \"Yellow\" { $PSStyle.Foreground.Yellow }\n            \"Cyan\" { $PSStyle.Foreground.Cyan }\n            \"Red\" { $PSStyle.Foreground.Red }\n            default { $PSStyle.Foreground.Cyan }\n        }\n\n        if ($Bold) {\n            Write-Host \"${colorCode}$($PSStyle.Bold)$Text$($PSStyle.Reset)\" -NoNewline\n        } else {\n            Write-Host \"${colorCode}$Text$($PSStyle.Reset)\" -NoNewline\n        }\n    } else {\n        # Fallback to no colors for older PowerShell\n        Write-Host $Text -NoNewline\n    }\n}\n\nfunction Write-PDMLog {\n    param([string]$Message)\n    Write-ColorOutput -Text \"PDM: \" -Color Green -Bold\n    Write-ColorOutput -Text $Message -Color Cyan\n    Write-Host\n}\n\nfunction Write-PDMWarning {\n    param([string]$Message)\n    Write-ColorOutput \"Warning: \" -Color Yellow\n    Write-Host $Message\n}\n\nfunction Write-PDMError {\n    param([string]$Message)\n    Write-ColorOutput \"Error: \" -Color Red\n    Write-Host $Message\n    exit 1\n}\n\n# Detect Windows platform and architecture\nfunction Get-Platform {\n    $arch = $env:PROCESSOR_ARCHITECTURE\n\n    switch ($arch) {\n        \"AMD64\" { $archName = \"x86_64\" }\n        \"ARM64\" { $archName = \"aarch64\" }\n        \"x86\" { $archName = \"i686\" }\n        default { Write-PDMError \"Unsupported architecture: $arch\" }\n    }\n\n    # Windows target triple\n    return \"${archName}-pc-windows-msvc\"\n}\n\n# Get the download URL from GitHub API\nfunction Get-DownloadUrl {\n    param(\n        [string]$Version,\n        [string]$Platform\n    )\n\n    $headers = @{}\n    if ($env:GITHUB_TOKEN) {\n        $headers[\"Authorization\"] = \"token $env:GITHUB_TOKEN\"\n    }\n\n    if ($Version -eq \"latest\" -or -not $Version) {\n        $apiUrl = \"https://api.github.com/repos/$Repo/releases/latest\"\n    } else {\n        $apiUrl = \"https://api.github.com/repos/$Repo/releases/tags/$Version\"\n    }\n\n    try {\n        $releaseInfo = Invoke-RestMethod -Uri $apiUrl -Headers $headers\n    } catch {\n        Write-PDMError \"Failed to fetch release info: $_\"\n    }\n\n    # Find the asset matching our platform\n    $pattern = \"pdm-.*-$Platform\\.tar\\.gz\"\n    $asset = $releaseInfo.assets | Where-Object { $_.name -match $pattern } | Select-Object -First 1\n\n    if (-not $asset) {\n        Write-PDMError \"No binary found for platform $Platform\"\n    }\n\n    return $asset.browser_download_url\n}\n\n# Download a file with progress\nfunction Invoke-Download {\n    param(\n        [string]$Url,\n        [string]$OutputPath\n    )\n\n    Write-PdmLog \"Downloading from $Url\"\n\n    try {\n        # Use Invoke-WebRequest with progress\n        $progressPreference = 'Continue'\n        Invoke-WebRequest -Uri $Url -OutFile $OutputPath -UseBasicParsing\n        $progressPreference = 'SilentlyContinue'\n    } catch {\n        Write-PDMError \"Download failed: $_\"\n    }\n}\n\n# Verify checksum using PowerShell's Get-FileHash\nfunction Test-Checksum {\n    param(\n        [string]$ChecksumFile,\n        [string]$FilePath\n    )\n\n    if ($SkipChecksum) {\n        Write-PDMLog \"Checksum verification skipped (--skip-checksum)\"\n        return $true\n    }\n\n    if (-not (Test-Path $ChecksumFile)) {\n        Write-PDMLog \"No checksum file found. Skipping verification.\"\n        return $true\n    }\n\n    Write-PDMLog \"Verifying checksum...\"\n\n    # Read expected checksum\n    $expectedChecksum = (Get-Content $ChecksumFile -Raw).Trim().Split()[0]\n\n    if (-not $expectedChecksum) {\n        Write-PDMLog \"No checksum found in file. Skipping verification.\"\n        return $true\n    }\n\n    # Calculate actual checksum\n    $actualChecksum = (Get-FileHash -Path $FilePath -Algorithm SHA256).Hash.ToLower()\n    $expectedChecksum = $expectedChecksum.ToLower()\n\n    if ($expectedChecksum -eq $actualChecksum) {\n        Write-PDMLog \"Checksum verification passed.\"\n        return $true\n    } else {\n        Write-PDMError \"Checksum verification failed!`nExpected: $expectedChecksum`nActual:   $actualChecksum\"\n    }\n}\n\n# Extract tar.gz archive\nfunction Expand-TarGz {\n    param(\n        [string]$ArchivePath,\n        [string]$DestinationPath\n    )\n\n    Write-PDMLog \"Extracting to $DestinationPath\"\n\n    # Check if tar is available (Git for Windows, WSL, etc.)\n    $tar = Get-Command tar -ErrorAction SilentlyContinue\n    if ($tar) {\n        try {\n            & $tar -xzf $ArchivePath -C $DestinationPath\n            return\n        } catch {\n            Write-PDMWarning \"tar extraction failed, trying alternative method\"\n        }\n    }\n\n    # Fallback: Extract in memory using .NET\n    try {\n        Add-Type -AssemblyName System.IO.Compression.FileSystem\n\n        # This is a simple approach - for tar.gz we'd need more complex handling\n        # Since PDM ships as .tar.gz, we'll use the windows tar if available\n        # or suggest installing it\n        Write-PDMError \"This script requires 'tar' to extract .tar.gz files. Please install Git for Windows or Windows Subsystem for Linux.\"\n    } catch {\n        Write-PDMError \"Failed to extract archive: $_\"\n    }\n}\n\n# Add directory to PATH in registry\nfunction Add-ToPath {\n    param([string]$BinPath)\n\n    if ($SkipAddToPath) {\n        Write-PDMLog \"Skipping PATH modification (--skip-add-to-path)\"\n        return\n    }\n\n    Write-PDMLog \"Adding $BinPath to user PATH...\"\n\n    try {\n        $regPath = \"Registry::HKEY_CURRENT_USER\\Environment\"\n        $currentPath = (Get-ItemProperty -Path $regPath -Name PATH -ErrorAction SilentlyContinue).PATH\n\n        # Check if already in PATH\n        if ($currentPath -and $currentPath -split ';' -contains $BinPath) {\n            Write-PDMLog \"Already in PATH\"\n            return\n        }\n\n        # Add to PATH\n        if ($currentPath) {\n            $newPath = $currentPath + \";\" + $BinPath\n        } else {\n            $newPath = $BinPath\n        }\n\n        Set-ItemProperty -Path $regPath -Name PATH -Value $newPath\n\n        Write-Host\n        Write-ColorOutput -Color Yellow -Text \"Note: \"\n        Write-ColorOutput -Color Cyan -Text \"Please restart your terminal or run:\"\n        Write-Host \"    `$env:PATH = '$BinPath;' + `$env:PATH\"\n        Write-Host\n    } catch {\n        Write-PDMWarning \"Failed to add to PATH: $_\"\n        Write-PDMLog \"Please manually add '$BinPath' to your PATH\"\n    }\n}\n\n# Verify PDM installation\nfunction Test-PdmInstall {\n    param([string]$PdmPath)\n\n    Write-PDMLog \"Verifying installation...\"\n\n    if (-not (Test-Path $PdmPath)) {\n        Write-PDMError \"PDM binary not found at $PdmPath\"\n    }\n\n    try {\n        $versionOutput = & $PdmPath --version 2>&1\n        if ($LASTEXITCODE -eq 0) {\n            return $versionOutput\n        } else {\n            Write-PDMError \"PDM verification failed: $versionOutput\"\n        }\n    } catch {\n        Write-PDMError \"PDM verification failed: $_\"\n    }\n}\n\n# Main installation function\nfunction Install-Pdm {\n    Write-ColorOutput -Color Green -Bold -Text \"PDM Installer for Windows\"\n    Write-Host\n\n    # Detect platform\n    $platform = Get-Platform\n    Write-PDMLog \"Detected platform: $platform\"\n\n    # Get download URL\n    $downloadUrl = Get-DownloadUrl -Version $Version -Platform $platform\n    Write-PDMLog \"Download URL: $downloadUrl\"\n\n    # Determine install directory\n    if (-not $InstallPath) {\n        $InstallPath = $DefaultInstallPath\n    }\n    Write-PDMLog \"Install directory: $InstallPath\"\n\n    # Create temp directory\n    $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())\n    New-Item -ItemType Directory -Path $tempDir -Force | Out-Null\n\n    try {\n        # Download files\n        $archivePath = Join-Path $tempDir \"pdm.tar.gz\"\n        $checksumPath = Join-Path $tempDir \"pdm.tar.gz.sha256\"\n\n        # Download checksum first (optional)\n        if (-not $SkipChecksum) {\n            try {\n                Invoke-Download -Url \"$downloadUrl.sha256\" -OutputPath $checksumPath\n            } catch {\n                Write-PDMLog \"No checksum file available, skipping verification\"\n                $SkipChecksum = $true\n            }\n        }\n\n        # Download binary\n        Invoke-Download -Url $downloadUrl -OutputPath $archivePath\n\n        # Verify checksum if available\n        if ((Test-Path $checksumPath) -and -not $SkipChecksum) {\n            if (-not (Test-Checksum -ChecksumFile $checksumPath -FilePath $archivePath)) {\n                return\n            }\n        }\n\n        # Extract archive\n        Expand-TarGz -ArchivePath $archivePath -DestinationPath $tempDir\n\n        # Find and copy binary\n        $binary = Get-ChildItem -Path $tempDir -Filter \"pdm.exe\" -Recurse | Select-Object -First 1\n        if (-not $binary) {\n            Write-PDMError \"PDM binary not found in archive\"\n        }\n\n        # Create install directory\n        $binPath = Join-Path $InstallPath \"bin\"\n        $null = New-Item -ItemType Directory -Path $binPath -Force\n\n        # Copy binary\n        $pdmPath = Join-Path $binPath \"pdm.exe\"\n        Copy-Item -Path $binary.FullName -Destination $pdmPath -Force\n\n        Write-PDMLog \"Successfully installed PDM\"\n\n        # Verify installation\n        $version = Test-PdmInstall -PdmPath $pdmPath\n        Write-Host\n        Write-ColorOutput -Color Green -Bold -Text \"Successfully installed: \"\n        Write-ColorOutput -Color Green -Text \"PDM\"\n        Write-ColorOutput -Color Yellow -Text \" ($version)\"\n        Write-ColorOutput -Color Cyan -Text \" at \"\n        Write-ColorOutput -Color Green -Text $pdmPath\n        Write-Host\n\n        # Add to PATH\n        Add-ToPath -BinPath $binPath\n\n        Write-PDMLog \"Installation completed successfully!\"\n\n    } finally {\n        # Cleanup\n        if (Test-Path $tempDir) {\n            Remove-Item -Path $tempDir -Recurse -Force\n        }\n    }\n}\n\n# Run installation\nInstall-Pdm\n"
  },
  {
    "path": "install-pdm.py",
    "content": "from __future__ import annotations\n\nimport argparse\nimport dataclasses\nimport io\nimport json\nimport os\nimport platform\nimport shutil\nimport site\nimport subprocess\nimport sys\nimport urllib.request\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom typing import Sequence\n\nif sys.version_info < (3, 8):  # noqa: UP036\n    sys.exit(\"Python 3.8 or above is required to install PDM.\")\n\n_plat = platform.system()\nMACOS = _plat == \"Darwin\"\nWINDOWS = _plat == \"Windows\"\nREPO = os.getenv(\"PDM_REPO\", \"https://github.com/pdm-project/pdm\")\n\nFOREGROUND_COLORS = {\n    \"black\": 30,\n    \"red\": 31,\n    \"green\": 32,\n    \"yellow\": 33,\n    \"blue\": 34,\n    \"magenta\": 35,\n    \"cyan\": 36,\n    \"white\": 37,\n}\n\n\ndef _call_subprocess(args: list[str]) -> int:\n    try:\n        return subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=True).returncode\n    except subprocess.CalledProcessError as e:\n        print(f\"An error occurred when executing {args}:\", file=sys.stderr)\n        print(e.output.decode(\"utf-8\"), file=sys.stderr)\n        sys.exit(e.returncode)\n\n\ndef _echo(text: str) -> None:\n    sys.stdout.write(text + \"\\n\")\n\n\nif WINDOWS:\n    import winreg\n\n    def _get_win_folder_with_ctypes(csidl_name: str) -> str:\n        import ctypes\n\n        csidl_const = {\n            \"CSIDL_APPDATA\": 26,\n            \"CSIDL_COMMON_APPDATA\": 35,\n            \"CSIDL_LOCAL_APPDATA\": 28,\n        }[csidl_name]\n\n        buf = ctypes.create_unicode_buffer(1024)\n        ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)\n\n        # Downgrade to short path name if have highbit chars. See\n        # <http://bugs.activestate.com/show_bug.cgi?id=85099>.\n        has_high_char = False\n        for c in buf:\n            if ord(c) > 255:\n                has_high_char = True\n                break\n        if has_high_char:\n            buf2 = ctypes.create_unicode_buffer(1024)\n            if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):\n                buf = buf2\n\n        return buf.value\n\n    def _get_win_folder_from_registry(csidl_name: str) -> str:\n        \"\"\"This is a fallback technique at best. I'm not sure if using the\n        registry for this guarantees us the correct answer for all CSIDL_*\n        names.\n        \"\"\"\n        shell_folder_name = {\n            \"CSIDL_APPDATA\": \"AppData\",\n            \"CSIDL_COMMON_APPDATA\": \"Common AppData\",\n            \"CSIDL_LOCAL_APPDATA\": \"Local AppData\",\n        }[csidl_name]\n\n        key = winreg.OpenKey(\n            winreg.HKEY_CURRENT_USER,\n            r\"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\",\n        )\n        dir, _ = winreg.QueryValueEx(key, shell_folder_name)\n        return dir\n\n    try:\n        from ctypes import windll  # noqa: F401\n\n        _get_win_folder = _get_win_folder_with_ctypes\n    except ImportError:\n        _get_win_folder = _get_win_folder_from_registry\n\n    def _remove_path_windows(target: Path) -> None:\n        value = os.path.normcase(target)\n\n        with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as root:\n            with winreg.OpenKey(root, \"Environment\", 0, winreg.KEY_ALL_ACCESS) as env_key:\n                try:\n                    old_value, type_ = winreg.QueryValueEx(env_key, \"PATH\")\n                    paths = [os.path.normcase(item) for item in old_value.split(os.pathsep)]\n                    if value not in paths:\n                        return\n\n                    new_value = os.pathsep.join(p for p in paths if p != value)\n                    winreg.SetValueEx(env_key, \"PATH\", 0, type_, new_value)\n                except FileNotFoundError:\n                    return\n\n\ndef _add_to_path(target: Path) -> None:\n    value = os.path.normcase(target)\n\n    if WINDOWS:\n        with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as root:\n            with winreg.OpenKey(root, \"Environment\", 0, winreg.KEY_ALL_ACCESS) as env_key:\n                try:\n                    old_value, type_ = winreg.QueryValueEx(env_key, \"PATH\")\n                    if value in [os.path.normcase(item) for item in old_value.split(os.pathsep)]:\n                        return\n                except FileNotFoundError:\n                    old_value, type_ = \"\", winreg.REG_EXPAND_SZ\n                new_value = os.pathsep.join([old_value, value]) if old_value else value\n                winreg.SetValueEx(env_key, \"PATH\", 0, type_, new_value)\n\n        _echo(\n            \"Post-install: {} is added to PATH env, please restart your terminal to take effect\".format(\n                colored(\"green\", value)\n            )\n        )\n    else:\n        paths = [os.path.normcase(p) for p in os.getenv(\"PATH\", \"\").split(os.pathsep)]\n        if value in paths:\n            return\n        _echo(\n            \"Post-install: Please add {} to PATH by executing:\\n    {}\".format(\n                colored(\"green\", value),\n                colored(\"cyan\", f\"export PATH={value}:$PATH\"),\n            )\n        )\n\n\ndef support_ansi() -> bool:\n    if WINDOWS:\n        return (\n            os.getenv(\"ANSICON\") is not None\n            or os.getenv(\"WT_SESSION\") is not None\n            or \"ON\" == os.getenv(\"ConEmuANSI\")\n            or \"xterm\" == os.getenv(\"Term\")\n        )\n\n    if not hasattr(sys.stdout, \"fileno\"):\n        return False\n\n    try:\n        return os.isatty(sys.stdout.fileno())\n    except io.UnsupportedOperation:\n        return False\n\n\ndef colored(color: str, text: str, bold: bool = False) -> str:\n    if not support_ansi():\n        return text\n    codes = [FOREGROUND_COLORS[color]]\n    if bold:\n        codes.append(1)\n\n    return \"\\x1b[{}m{}\\x1b[0m\".format(\";\".join(map(str, codes)), text)\n\n\n@dataclasses.dataclass\nclass Installer:\n    location: str | None = None\n    version: str | None = None\n    prerelease: bool = False\n    additional_deps: Sequence[str] = ()\n    skip_add_to_path: bool = False\n    output_path: str | None = None\n    frozen_deps: bool = True\n\n    def __post_init__(self):\n        self._path = self._decide_path()\n        self._path.mkdir(parents=True, exist_ok=True)\n\n    def _decide_path(self) -> Path:\n        if self.location is not None:\n            return Path(self.location).expanduser().resolve()\n\n        if WINDOWS:\n            const = \"CSIDL_APPDATA\"\n            path = os.path.normpath(_get_win_folder(const))\n            path = os.path.join(path, \"pdm\")\n        elif MACOS:\n            path = os.path.expanduser(\"~/Library/Application Support/pdm\")\n        else:\n            path = os.getenv(\"XDG_DATA_HOME\", os.path.expanduser(\"~/.local/share\"))\n            path = os.path.join(path, \"pdm\")\n\n        return Path(path).resolve()\n\n    def _make_env(self) -> Path:\n        venv_path = self._path / \"venv\"\n\n        _echo(\n            \"Installing {} ({}): {}\".format(\n                colored(\"green\", \"PDM\", bold=True),\n                colored(\"yellow\", self.version or \"latest\"),\n                colored(\"cyan\", \"Creating virtual environment\"),\n            )\n        )\n\n        try:\n            import venv\n\n            venv.create(venv_path, clear=False, with_pip=True)\n        except (ModuleNotFoundError, subprocess.CalledProcessError):\n            try:\n                import virtualenv\n            except ModuleNotFoundError:\n                python_version = f\"{sys.version_info.major}.{sys.version_info.minor}\"\n                url = f\"https://bootstrap.pypa.io/virtualenv/{python_version}/virtualenv.pyz\"\n                with TemporaryDirectory(prefix=\"pdm-installer-\") as tempdir:\n                    virtualenv_zip = Path(tempdir) / \"virtualenv.pyz\"\n                    urllib.request.urlretrieve(url, virtualenv_zip)\n                    _call_subprocess([sys.executable, str(virtualenv_zip), str(venv_path)])\n            else:\n                virtualenv.cli_run([str(venv_path)])\n\n        return venv_path\n\n    def _install(self, venv_path: Path) -> None:\n        _echo(\n            \"Installing {} ({}): {}\".format(\n                colored(\"green\", \"PDM\", bold=True),\n                colored(\"yellow\", self.version or \"latest\"),\n                colored(\"cyan\", \"Installing PDM and dependencies\"),\n            )\n        )\n\n        if WINDOWS:\n            venv_python = venv_path / \"Scripts/python.exe\"\n        else:\n            venv_python = venv_path / \"bin/python\"\n\n        # Re-install the venv pip to ensure it's not DEBUNDLED\n        # See issue/685\n        try:\n            _call_subprocess([str(venv_python), \"-m\", \"ensurepip\"])\n        except SystemExit:\n            pass\n        _call_subprocess([str(venv_python), \"-m\", \"pip\", \"install\", \"-IU\", \"pip\"])\n\n        locked = \"[locked]\" if self.frozen_deps else \"\"\n        if self.version:\n            if self.version.upper() == \"HEAD\":\n                req = f\"pdm{locked} @ git+{REPO}.git@main\"\n            else:\n                try:\n                    parsed = tuple(map(int, self.version.split(\".\")))\n                except ValueError:\n                    extra = \"\"\n                else:\n                    extra = locked if parsed >= (2, 17) else \"\"\n                req = f\"pdm{extra}=={self.version}\"\n        else:\n            req = f\"pdm{locked}\"\n        args = [req] + [d for d in self.additional_deps if d]\n        pip_cmd = [str(venv_python), \"-Im\", \"pip\", \"install\", *args]\n        _call_subprocess(pip_cmd)\n\n    def _make_bin(self, venv_path: Path) -> Path:\n        if self.location:\n            bin_path = self._path / \"bin\"\n        else:\n            userbase = Path(site.getuserbase())\n            bin_path = userbase / (\"Scripts\" if WINDOWS else \"bin\")\n\n        _echo(\n            \"Installing {} ({}): {} {}\".format(\n                colored(\"green\", \"PDM\", bold=True),\n                colored(\"yellow\", self.version or \"latest\"),\n                colored(\"cyan\", \"Making binary at\"),\n                colored(\"green\", str(bin_path)),\n            )\n        )\n        bin_path.mkdir(parents=True, exist_ok=True)\n        if WINDOWS:\n            script = bin_path / \"pdm.exe\"\n            target = venv_path / \"Scripts\" / \"pdm.exe\"\n        else:\n            script = bin_path / \"pdm\"\n            target = venv_path / \"bin\" / \"pdm\"\n\n        if script.exists():\n            script.unlink()\n        try:\n            script.symlink_to(target)\n        except OSError:\n            shutil.copy(target, script)\n        return bin_path\n\n    def _post_install(self, venv_path: Path, bin_path: Path) -> None:\n        if WINDOWS:\n            script = bin_path / \"pdm.exe\"\n            python = venv_path / \"Scripts/python.exe\"\n        else:\n            script = bin_path / \"pdm\"\n            python = venv_path / \"bin/python\"\n        subprocess.check_call([str(script), \"--help\"])\n        print()\n        pdm_version = (\n            subprocess.check_output([python, \"-c\", 'from importlib.metadata import version; print(version(\"pdm\"))'])\n            .decode(\"utf-8\")\n            .strip()\n        )\n        _echo(\n            \"Successfully installed: {} ({}) at {}\".format(\n                colored(\"green\", \"PDM\", bold=True),\n                colored(\"yellow\", pdm_version),\n                colored(\"cyan\", str(script)),\n            )\n        )\n        if not self.skip_add_to_path:\n            _add_to_path(bin_path)\n        self._write_output(venv_path, script, pdm_version)\n\n    def _write_output(self, venv_path: Path, script: Path, pdm_version: str) -> None:\n        if not self.output_path:\n            return\n        print(\"Writing output to\", colored(\"green\", self.output_path))\n        output = {\n            \"pdm_version\": pdm_version,\n            \"pdm_bin\": str(script),\n            \"install_python_version\": platform.python_version(),\n            \"install_location\": str(venv_path),\n        }\n        with open(self.output_path, \"w\") as f:\n            json.dump(output, f, indent=2)\n\n    def install(self) -> None:\n        venv = self._make_env()\n        self._install(venv)\n        bin_dir = self._make_bin(venv)\n        self._post_install(venv, bin_dir)\n\n    def uninstall(self) -> None:\n        venv_path = self._path / \"venv\"\n        if not venv_path.exists():\n            _echo(\n                \"{} is not currently installed.\".format(\n                    colored(\"green\", \"PDM\", bold=True),\n                )\n            )\n            return\n\n        _echo(\n            \"Uninstalling {}: {}\".format(\n                colored(\"green\", \"PDM\", bold=True),\n                colored(\"cyan\", \"Removing venv and script\"),\n            )\n        )\n        if self.location:\n            bin_path = self._path / \"bin\"\n        else:\n            userbase = Path(site.getuserbase())\n            bin_path = userbase / (\"Scripts\" if WINDOWS else \"bin\")\n\n        if WINDOWS:\n            script = bin_path / \"pdm.exe\"\n        else:\n            script = bin_path / \"pdm\"\n\n        shutil.rmtree(venv_path)\n        script.unlink()\n\n        if WINDOWS:\n            _remove_path_windows(bin_path)\n\n        print()\n        _echo(\"Successfully uninstalled\")\n\n\ndef main():\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\n        \"-v\",\n        \"--version\",\n        help=\"Specify the version to be installed, or HEAD to install from the main branch\",\n        default=os.getenv(\"PDM_VERSION\"),\n    )\n    parser.add_argument(\n        \"--prerelease\",\n        action=\"store_true\",\n        help=\"Allow prereleases to be installed\",\n        default=os.getenv(\"PDM_PRERELEASE\"),\n    )\n    parser.add_argument(\n        \"--no-frozen-deps\",\n        action=\"store_false\",\n        dest=\"frozen_deps\",\n        default=not bool(os.getenv(\"PDM_NO_FROZEN_DEPS\")),\n        help=\"Do not install frozen dependency versions\",\n    )\n    parser.add_argument(\n        \"--remove\",\n        action=\"store_true\",\n        help=\"Remove the PDM installation\",\n        default=os.getenv(\"PDM_REMOVE\"),\n    )\n    parser.add_argument(\n        \"-p\",\n        \"--path\",\n        help=\"Specify the location to install PDM\",\n        default=os.getenv(\"PDM_HOME\"),\n    )\n    parser.add_argument(\n        \"-d\",\n        \"--dep\",\n        action=\"append\",\n        default=os.getenv(\"PDM_DEPS\", \"\").split(\",\"),\n        help=\"Specify additional dependencies, can be given multiple times\",\n    )\n    parser.add_argument(\n        \"--skip-add-to-path\",\n        action=\"store_true\",\n        help=\"Do not add binary to the PATH.\",\n        default=os.getenv(\"PDM_SKIP_ADD_TO_PATH\"),\n    )\n    parser.add_argument(\"-o\", \"--output\", help=\"Output file to write the installation info to\")\n\n    options = parser.parse_args()\n    installer = Installer(\n        location=options.path,\n        version=options.version,\n        prerelease=options.prerelease,\n        additional_deps=options.dep,\n        skip_add_to_path=options.skip_add_to_path,\n        output_path=options.output,\n        frozen_deps=options.frozen_deps,\n    )\n    if options.remove:\n        installer.uninstall()\n    else:\n        installer.install()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "install-pdm.py.sha256",
    "content": "619d9d97d4085b3a244055d45aa191caceb586f8aaa694e0db8842171f5c9db9  install-pdm.py\n"
  },
  {
    "path": "install-pdm.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\n# PDM Installer Script\n# Downloads and installs PDM from GitHub released binaries\n\nREPO=\"pdm-project/pdm\"\nINSTALL_DIR=\"${PDM_HOME:-}\"\nVERSION=\"${PDM_VERSION:-latest}\"\nSKIP_ADD_TO_PATH=\"${PDM_SKIP_ADD_TO_PATH:-false}\"\nSKIP_CHECKSUM=\"${PDM_SKIP_CHECKSUM:-false}\"\n\n# Color output\nif [ -t 1 ]; then\n    RED='\\033[0;31m'\n    GREEN='\\033[0;32m'\n    YELLOW='\\033[0;33m'\n    CYAN='\\033[0;36m'\n    BOLD='\\033[1m'\n    NC='\\033[0m' # No Color\nelse\n    RED=''\n    GREEN=''\n    YELLOW=''\n    CYAN=''\n    BOLD=''\n    NC=''\nfi\n\nlog() {\n    echo -e \"${GREEN}${BOLD}PDM${NC}: ${CYAN}$1${NC}\"\n}\n\nwarn() {\n    echo -e \"${YELLOW}Warning:${NC} $1\" >&2\n}\n\nerror() {\n    echo -e \"${RED}Error:${NC} $1\" >&2\n    exit 1\n}\n\n# Detect OS and architecture\ndetect_platform() {\n    local os arch target\n\n    # Detect OS\n    case \"$(uname -s)\" in\n        Linux)\n            os=\"unknown-linux-gnu\"\n            ;;\n        Darwin)\n            os=\"apple-darwin\"\n            ;;\n        MINGW* | MSYS* | CYGWIN* | Windows_NT)\n            os=\"pc-windows-msvc\"\n            ;;\n        *)\n            error \"Unsupported OS: $(uname -s)\"\n            ;;\n    esac\n\n    # Detect architecture\n    case \"$(uname -m)\" in\n        x86_64 | amd64)\n            arch=\"x86_64\"\n            ;;\n        aarch64 | arm64)\n            arch=\"aarch64\"\n            ;;\n        armv7l)\n            arch=\"armv7\"\n            ;;\n        i686 | i386)\n            arch=\"i686\"\n            ;;\n        *)\n            error \"Unsupported architecture: $(uname -m)\"\n            ;;\n    esac\n\n    # Construct target triple\n    target=\"${arch}-${os}\"\n\n    # Handle special cases\n    if [ \"$os\" = \"apple-darwin\" ] && [ \"$arch\" = \"aarch64\" ]; then\n        # aarch64-apple-darwin is the correct name for ARM64 Macs\n        target=\"aarch64-apple-darwin\"\n    elif [ \"$os\" = \"pc-windows-msvc\" ] && [ \"$arch\" = \"i686\" ]; then\n        # Windows 32-bit (limited support)\n        target=\"i686-pc-windows-msvc\"\n    fi\n\n    echo \"$target\"\n}\n\n# Get the download URL for a specific version\nget_download_url() {\n    local version=\"$1\"\n    local platform=\"$2\"\n    local release_url\n\n    if [ \"$version\" = \"latest\" ]; then\n        release_url=\"https://api.github.com/repos/${REPO}/releases/latest\"\n    else\n        release_url=\"https://api.github.com/repos/${REPO}/releases/tags/${version}\"\n    fi\n\n    # Use curl or wget\n    local json\n    if command -v curl >/dev/null 2>&1; then\n        json=$(curl -s \"$release_url\")\n    elif command -v wget >/dev/null 2>&1; then\n        json=$(wget -qO- \"$release_url\")\n    else\n        error \"Neither curl nor wget found. Please install one of them.\"\n    fi\n\n    # Extract download URL for the platform\n    # PDM binaries are named like: pdm-2.26.1-x86_64-unknown-linux-gnu.tar.gz\n    local pattern=\"pdm-[^-]*-${platform}\\\\.tar\\\\.gz\"\n    local url\n    url=$(echo \"$json\" | grep -o \"https://github.com[^\\\"]*${pattern}\" | head -1)\n\n    if [ -z \"$url\" ]; then\n        error \"No binary found for platform ${platform}\"\n    fi\n\n    echo \"$url\"\n}\n\n# Determine install directory\ndetermine_install_dir() {\n    if [ -n \"$INSTALL_DIR\" ]; then\n        echo \"$INSTALL_DIR\"\n    else\n        # Default to ~/.local/bin on Unix-like systems\n        echo \"$HOME/.local/bin\"\n    fi\n}\n\n# Download and extract PDM\ndownload_and_install() {\n    local url=\"$1\"\n    local install_dir=\"$2\"\n    local temp_dir temp_file checksum_file\n\n    temp_dir=$(mktemp -d)\n    temp_file=\"${temp_dir}/pdm.tar.gz\"\n    checksum_file=\"${temp_dir}/pdm.tar.gz.sha256\"\n\n    log \"Downloading PDM from $url\"\n\n    # First try to download checksum file\n    local checksum_url=\"${url}.sha256\"\n    local checksum_downloaded=false\n\n    if command -v curl >/dev/null 2>&1; then\n        if curl -sL -o \"$checksum_file\" \"$checksum_url\" 2>/dev/null; then\n            if [ -s \"$checksum_file\" ]; then\n                checksum_downloaded=true\n            fi\n        fi\n    elif command -v wget >/dev/null 2>&1; then\n        if wget -O \"$checksum_file\" \"$checksum_url\" 2>/dev/null; then\n            if [ -s \"$checksum_file\" ]; then\n                checksum_downloaded=true\n            fi\n        fi\n    fi\n\n    # Download the binary\n    if command -v curl >/dev/null 2>&1; then\n        curl -sL -o \"$temp_file\" \"$url\"\n    elif command -v wget >/dev/null 2>&1; then\n        wget -O \"$temp_file\" \"$url\"\n    fi\n\n    # Verify checksum if we downloaded it\n    if [ \"$checksum_downloaded\" = true ]; then\n        verify_checksum_from_file \"$checksum_file\" \"$temp_file\"\n    else\n        log \"No checksum file found. Skipping verification.\"\n    fi\n\n    log \"Extracting to $install_dir\"\n\n    # Create install directory\n    mkdir -p \"$install_dir\"\n\n    # Extract tar.gz file\n    if command -v tar >/dev/null 2>&1; then\n        tar -xzf \"$temp_file\" -C \"$temp_dir\"\n    else\n        error \"tar not found. Please install tar to extract the archive.\"\n    fi\n\n    # Find and copy the binary\n    local binary\n    if [ \"$(uname -s)\" = \"Linux\" ] || [ \"$(uname -s)\" = \"Darwin\" ]; then\n        binary=$(find \"$temp_dir\" -name \"pdm\" -type f | head -1)\n        if [ -z \"$binary\" ]; then\n            error \"PDM binary not found in archive\"\n        fi\n        cp \"$binary\" \"$install_dir/pdm\"\n        chmod +x \"$install_dir/pdm\"\n    else\n        # Windows\n        binary=$(find \"$temp_dir\" -name \"pdm.exe\" -type f | head -1)\n        if [ -z \"$binary\" ]; then\n            error \"PDM binary not found in archive\"\n        fi\n        cp \"$binary\" \"$install_dir/pdm.exe\"\n    fi\n\n    # Cleanup\n    rm -rf \"$temp_dir\"\n}\n\n# Verify checksum from a checksum file\nverify_checksum_from_file() {\n    local checksum_file=\"$1\"\n    local file_path=\"$2\"\n    local expected_checksum actual_checksum\n\n    # Skip verification if explicitly disabled\n    if [ \"$SKIP_CHECKSUM\" = \"true\" ]; then\n        log \"Checksum verification skipped (--skip-checksum).\"\n        return 0\n    fi\n\n    # Read expected checksum from file\n    expected_checksum=$(awk '{print $1}' \"$checksum_file\")\n\n    # If no checksum found, skip verification\n    if [ -z \"$expected_checksum\" ]; then\n        log \"No checksum found in file. Skipping verification.\"\n        return 0\n    fi\n\n    # Check if sha256sum is available\n    if ! command -v sha256sum >/dev/null 2>&1; then\n        log \"sha256sum not found. Skipping verification.\"\n        return 0\n    fi\n\n    log \"Verifying checksum...\"\n\n    # Calculate actual checksum\n    actual_checksum=$(sha256sum \"$file_path\" | awk '{print $1}')\n\n    if [ \"$expected_checksum\" = \"$actual_checksum\" ]; then\n        log \"Checksum verification passed.\"\n        return 0\n    else\n        error \"Checksum verification failed!\"\n        echo \"Expected: $expected_checksum\"\n        echo \"Actual:   $actual_checksum\"\n        return 1\n    fi\n}\n\n# Add to PATH\nadd_to_path() {\n    local bin_dir=\"$1\"\n\n    if [ \"$SKIP_ADD_TO_PATH\" = \"true\" ]; then\n        return 0\n    fi\n\n    case \"$(uname -s)\" in\n        Linux* | Darwin*)\n            # Detect shell\n            local shell_name=\"$SHELL\"\n            local rcfile=\"\"\n\n            if [[ \"$shell_name\" == *bash* ]]; then\n                rcfile=\"$HOME/.bashrc\"\n            elif [[ \"$shell_name\" == *zsh* ]]; then\n                rcfile=\"$HOME/.zshrc\"\n            elif [[ \"$shell_name\" == *fish* ]]; then\n                rcfile=\"$HOME/.config/fish/config.fish\"\n            else\n                warn \"Cannot detect shell. Please manually add $bin_dir to your PATH.\"\n                return 0\n            fi\n\n            # Check if already in PATH\n            if echo \":$PATH:\" | grep -q \":${bin_dir}:\"; then\n                return 0\n            fi\n\n            # Add to rcfile\n            log \"Adding $bin_dir to PATH in $rcfile\"\n            if [[ \"$shell_name\" == *fish* ]]; then\n                echo \"set -gx PATH $bin_dir \\$PATH\" >> \"$rcfile\"\n            else\n                echo \"export PATH=\\\"$bin_dir:\\$PATH\\\"\" >> \"$rcfile\"\n            fi\n\n            echo\n            echo -e \"${YELLOW}Please restart your terminal or run:${NC}\"\n            if [[ \"$shell_name\" == *fish* ]]; then\n                echo -e \"${CYAN}    source $rcfile${NC}\"\n            else\n                echo -e \"${CYAN}    source $rcfile${NC}\"\n            fi\n            ;;\n        MINGW* | MSYS* | CYGWIN* | Windows_NT)\n            warn \"Please manually add $bin_dir to your PATH environment variable.\"\n            ;;\n    esac\n}\n\n# Verify installation\nverify_installation() {\n    local bin_path=\"$1\"\n    local pdm_cmd\n\n    if [ \"$(uname -s)\" = \"Linux\" ] || [ \"$(uname -s)\" = \"Darwin\" ]; then\n        pdm_cmd=\"$bin_path/pdm\"\n    else\n        pdm_cmd=\"$bin_path/pdm.exe\"\n    fi\n\n    if [ ! -x \"$pdm_cmd\" ]; then\n        error \"PDM binary not found or not executable at $pdm_cmd\"\n    fi\n\n    log \"Verifying installation...\"\n    \"$pdm_cmd\" --version\n}\n\n# Print help\nusage() {\n    cat << EOF\nPDM Installer - Install PDM from GitHub released binaries\n\nUsage: $0 [OPTIONS]\n\nOPTIONS:\n    -v, --version VERSION    Install specific version (default: latest)\n    -p, --path PATH          Installation directory (default: ~/.local/bin)\n    -h, --help               Show this help message\n        --skip-add-to-path   Skip adding to PATH\n        --skip-checksum      Skip checksum verification\n\nENVIRONMENT VARIABLES:\n    PDM_VERSION              Version to install (overridden by -v)\n    PDM_HOME                 Installation directory (overridden by -p)\n    PDM_SKIP_ADD_TO_PATH     Whether to skip adding to PATH (overridden by --skip-add-to-path)\n    PDM_SKIP_CHECKSUM        Whether to skip checksum verification (overridden by --skip-checksum)\n\nExamples:\n    $0                       # Install latest version\n    $0 -v 2.17.0             # Install version 2.17.0\n    $0 -p /usr/local/bin     # Install to /usr/local/bin\n    $0 --skip-checksum       # Skip checksum verification\n\nEOF\n}\n\n# Parse command line arguments\nwhile [[ $# -gt 0 ]]; do\n    case $1 in\n        -v|--version)\n            VERSION=\"$2\"\n            shift 2\n            ;;\n        -p|--path)\n            INSTALL_DIR=\"$2\"\n            shift 2\n            ;;\n        --skip-add-to-path)\n            SKIP_ADD_TO_PATH=\"true\"\n            shift\n            ;;\n        --skip-checksum)\n            SKIP_CHECKSUM=\"true\"\n            shift\n            ;;\n        -h|--help)\n            usage\n            exit 0\n            ;;\n        *)\n            error \"Unknown option: $1\"\n            ;;\n    esac\ndone\n\n# Main installation process\nmain() {\n    log \"Detecting platform...\"\n    local platform\n    platform=$(detect_platform)\n    echo \"  Platform: $platform\"\n\n    log \"Getting download URL for $VERSION...\"\n    local download_url\n    download_url=$(get_download_url \"$VERSION\" \"$platform\")\n    echo \"  URL: $download_url\"\n\n    local install_dir\n    install_dir=$(determine_install_dir)\n    echo \"  Install directory: $install_dir\"\n\n    # Install\n    download_and_install \"$download_url\" \"$install_dir\"\n    echo\n\n    # Verify\n    verify_installation \"$install_dir\"\n    echo\n\n    # Add to PATH\n    add_to_path \"$install_dir\"\n\n    log \"Installation completed successfully!\"\n}\n\n# Run main function\nmain \"$@\"\n"
  },
  {
    "path": "mkdocs.yml",
    "content": "site_name: PDM\nsite_url: https://pdm-project.org\n\nrepo_url: https://github.com/pdm-project/pdm\nedit_uri: edit/main/docs\n\ntheme:\n  name: material\n  palette:\n    - media: \"(prefers-color-scheme)\"\n      toggle:\n        icon: material/brightness-auto\n        name: Switch to light mode\n    - scheme: default\n      media: \"(prefers-color-scheme: light)\"\n      primary: deep purple\n      accent: teal\n      toggle:\n        icon: material/brightness-7\n        name: Switch to dark mode\n    - scheme: slate\n      media: \"(prefers-color-scheme: dark)\"\n      primary: deep purple\n      accent: teal\n      toggle:\n        icon: material/brightness-4\n        name: Switch to system preference\n  font:\n    text: Open Sans\n    code: Fira Code\n  logo: assets/logo.svg\n  favicon: assets/logo.svg\n  features:\n    - content.code.copy\n    - navigation.tabs\n    - navigation.tabs.sticky\n  custom_dir: docs/overrides\n\nplugins:\n  - search\n  - markdown-exec\n  - \"mkdocs-version-annotations\":\n      version_added_admonition: \"tip\"\n  - mkdocstrings:\n      enable_inventory: true\n      handlers:\n        python:\n          options:\n            docstring_style: google\n  - redirects:\n      redirect_maps:\n        'plugin/fixtures.md': 'dev/fixtures.md'\n        'plugin/write.md': 'dev/write.md'\n        'pyproject/build.md': 'reference/build.md'\n        'plugin/reference.md': 'reference/api.md'\n        'usage/cli_reference.md': 'reference/cli.md'\n        'usage/configuration.md': 'reference/configuration.md'\n        'pyproject/pep621.md': 'reference/pep621.md'\n  - llmstxt:\n      full_output: llms-full.txt\n      sections:\n        Usage:\n          - \"index.md\"\n          - \"usage/*.md\"\n        Reference:\n          - \"reference/*.md\"\n        Development:\n          - \"dev/*.md\"\n\nnav:\n  - Usage:\n      - Introduction: index.md\n      - usage/project.md\n      - usage/dependency.md\n      - Lock Files:\n        - usage/lockfile.md\n        - usage/lock-targets.md\n      - usage/uv.md\n      - usage/publish.md\n      - usage/config.md\n      - usage/scripts.md\n      - usage/hooks.md\n      - usage/advanced.md\n      - usage/venv.md\n      - usage/pep582.md\n      - usage/template.md\n  - Reference:\n      - reference/pep621.md\n      - reference/configuration.md\n      - reference/build.md\n      - reference/cli.md\n      - reference/api.md\n  - Development:\n      - dev/write.md\n      - dev/fixtures.md\n      - dev/contributing.md\n      - dev/changelog.md\n      - dev/benchmark.md\n  - Sponsor: https://github.com/sponsors/pdm-project\n\nmarkdown_extensions:\n  - pymdownx.highlight:\n      linenums: true\n  - pymdownx.tabbed:\n      alternate_style: true\n  - pymdownx.details\n  - pymdownx.snippets:\n      restrict_base_path: false\n  - admonition\n  - tables\n  - toc:\n      permalink: \"#\"\n  - attr_list\n  - pymdownx.emoji:\n      emoji_index: !!python/name:material.extensions.emoji.twemoji\n      emoji_generator: !!python/name:material.extensions.emoji.to_svg\n  - pymdownx.superfences:\n      custom_fences:\n        - name: mermaid\n          class: mermaid\n          format: !!python/name:pymdownx.superfences.fence_code_format\n\ncopyright: Copyright &copy; 2019 <a href=\"https://frostming.com\">Frost Ming</a>\n\nextra:\n  version:\n    provider: mike\n  analytics:\n    provider: google\n    property: G-RP4PM5PGLN\n  social:\n    - icon: fontawesome/brands/github\n      link: https://github.com/pdm-project/pdm\n    - icon: fontawesome/brands/twitter\n      link: https://twitter.com/pdm_project\n    - icon: fontawesome/brands/discord\n      link: https://discord.gg/Phn8smztpv\n  chatbot:\n    url: https://2prxfnwkygf4vexczrbpcq.streamlit.app/?embed=true\n  alternate:\n    - name: '🇬🇧 English'\n      link: /en/\n      lang: en\n    - name: '🇨🇳 简体中文'\n      link: /zh-cn/\n      lang: zh\nextra_css:\n  - assets/extra.css\nextra_javascript:\n  - assets/extra.js\n\nwatch:\n  - src\n"
  },
  {
    "path": "news/.gitkeep",
    "content": ""
  },
  {
    "path": "news/3541.misc.md",
    "content": "Add comprehensive tests for completion, show, search, and info commands to improve test coverage\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"pdm-backend\", \"pdm-build-locked\"]\nbuild-backend = \"pdm.backend\"\n\n[project]\n# PEP 621 project metadata\n# See https://www.python.org/dev/peps/pep-0621/\nname = \"pdm\"\ndescription = \"A modern Python package and dependency manager supporting the latest PEP standards\"\nauthors = [\n    {name = \"Frost Ming\", email = \"mianghong@gmail.com\"},\n]\ndynamic = [\"version\"]\nrequires-python = \">=3.9\"\nlicense = \"MIT\"\nlicense-files = [\"LICENSE\"]\ndependencies = [\n    \"blinker\",\n    \"packaging>22.0\",\n    \"platformdirs\",\n    \"rich>=12.3.0\",\n    \"virtualenv>=20\",\n    \"pyproject-hooks\",\n    \"unearth>=0.17.5\",\n    \"dep-logic>=0.5\",\n    \"findpython>=0.7.0,<1.0.0a0\",\n    \"tomlkit>=0.11.1,<1\",\n    \"shellingham>=1.3.2\",\n    \"python-dotenv>=0.15\",\n    \"resolvelib>=1.1\",\n    \"installer<0.8,>=0.7\",\n    \"truststore>=0.10.4; python_version >= \\\"3.10\\\"\",\n    \"tomli>=1.1.0; python_version < \\\"3.11\\\"\",\n    \"importlib-metadata>=3.6; python_version < \\\"3.10\\\"\",\n    \"hishel[httpx]>=1.0.0\",\n    \"pbs-installer>=2025.10.7\",\n    \"httpx[socks]<1,>0.20\",\n    \"filelock>=3.13\",\n    \"httpcore>=1.0.6\",\n    \"certifi>=2024.8.30\",\n    \"id>=1.5.0\"\n]\nreadme = \"README.md\"\nkeywords = [\"packaging\", \"dependency\", \"workflow\"]\nclassifiers = [\n    \"Topic :: Software Development :: Build Tools\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.9\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n]\n\n[project.urls]\nHomepage = \"https://pdm-project.org\"\nRepository = \"https://github.com/pdm-project/pdm\"\nDocumentation = \"https://pdm-project.org\"\nChangelog = \"https://pdm-project.org/latest/dev/changelog/\"\n\n[project.optional-dependencies]\npytest = [\n    \"pytest\",\n    \"pytest-mock\",\n]\ncopier = [\"copier>=8.0.0\"]\ncookiecutter = [\"cookiecutter\"]\nkeyring = [\"keyring\"]\ntemplate = [\n    \"pdm[copier,cookiecutter]\",\n]\nall = [\n    \"pdm[keyring,template]\",\n]\n\n[project.scripts]\npdm = \"pdm.core:main\"\n\n[dependency-groups]\ntest = [\n    \"pdm[pytest]\",\n    \"pytest-cov\",\n    \"pytest-xdist>=1.31.0\",\n    \"pytest-rerunfailures>=10.2\",\n    \"pytest-httpserver>=1.0.6\",\n    \"pytest-httpx>=0.34.0\",\n]\ntox = [\n    \"tox\",\n    \"tox-pdm>=0.5\",\n]\ndoc = [\n    \"mkdocs>=1.1\",\n    \"mkdocs-material>=7.3\",\n    \"mkdocstrings[python]>=0.18\",\n    \"setuptools>=62.3.3\",\n    \"markdown-exec>=0.7.0\",\n    \"mkdocs-redirects>=1.2.0\",\n    \"mkdocs-version-annotations>=1.0.0\",\n    \"mkdocs-llmstxt>=0.2.0\",\n]\nworkflow = [\n    \"parver>=0.3.1\",\n    \"towncrier>=20\",\n    \"pycomplete~=0.3\",\n]\n\n[tool.ruff]\nline-length = 120\nexclude = [\"tests/fixtures\"]\ntarget-version = \"py38\"\nsrc = [\"src\"]\n\n[tool.ruff.lint]\nextend-select = [\n  \"I\",    # isort\n  \"B\",    # flake8-bugbear\n  \"C4\",   # flake8-comprehensions\n  \"FA\",   # flake8-future-annotations\n  \"PGH\",  # pygrep-hooks\n  \"RUF\",  # ruff\n  \"W\",    # pycodestyle\n  \"UP\",   # pyupgrade\n  \"YTT\",  # flake8-2020\n]\nextend-ignore = [\"B018\", \"B019\", \"RUF018\"]\n\n[tool.ruff.lint.mccabe]\nmax-complexity = 10\n\n[tool.ruff.lint.isort]\nknown-first-party = [\"pdm\"]\n\n[tool.towncrier]\npackage = \"pdm\"\nfilename = \"CHANGELOG.md\"\nissue_format = \"[#{issue}](https://github.com/pdm-project/pdm/issues/{issue})\"\ndirectory = \"news/\"\ntitle_format = \"## Release v{version} ({project_date})\"\nunderlines = [\"\", \"\", \"\"]\n\n  [[tool.towncrier.type]]\n  directory = \"break\"\n  name = \"Breaking Changes\"\n  showcontent = true\n\n  [[tool.towncrier.type]]\n  directory = \"feature\"\n  name = \"Features & Improvements\"\n  showcontent = true\n\n  [[tool.towncrier.type]]\n  directory = \"bugfix\"\n  name = \"Bug Fixes\"\n  showcontent = true\n\n  [[tool.towncrier.type]]\n  directory = \"doc\"\n  name = \"Documentation\"\n  showcontent = true\n\n  [[tool.towncrier.type]]\n  directory = \"dep\"\n  name = \"Dependencies\"\n  showcontent = true\n\n  [[tool.towncrier.type]]\n  directory = \"removal\"\n  name = \"Removals and Deprecations\"\n  showcontent = true\n\n  [[tool.towncrier.type]]\n  directory = \"misc\"\n  name = \"Miscellany\"\n  showcontent = true\n\n[tool.pytest.ini_options]\nfilterwarnings = [\n  \"ignore::DeprecationWarning\"\n]\nmarkers = [\n    \"network: Tests that require network\",\n    \"integration: Run with all Python versions\",\n    \"path: Tests that compare with the system paths\",\n    \"deprecated: Tests about deprecated features\",\n    \"uv: Tests that require uv to be installed\",\n]\naddopts = \"-r aR\"\ntestpaths = [\n    \"tests/\",\n]\n\n[tool.codespell]\nignore-words-list = \"ba,overriden,te,instal\"\n\n[tool.coverage.run]\nbranch = true\nsource = [\"pdm\"]\nomit = [\n    \"*/pdm/__main__.py\",\n    \"*/pdm/pep582/sitecustomize.py\",\n    \"*/pdm/models/in_process/*.py\",\n    \"*/pdm-test-*-env/*\",\n    \"*/pdm/misc/sysconfig_patcher.py\",\n]\n\n[tool.coverage.report]\n# Regexes for lines to exclude from consideration\nexclude_lines = [\n    \"pragma: no cover\",\n    # Don't complain about missing debug-only code:\n    \"def __repr__\",\n    \"if self.debug\",\n    # Don't complain if tests don't hit defensive assertion code:\n    \"raise AssertionError\",\n    \"raise NotImplementedError\",\n    # Don't complain if non-runnable code isn't run:\n    \"if __name__ == .__main__.:\",\n    \"if TYPE_CHECKING:\",\n]\nignore_errors = true\n\n[tool.mypy]\nfollow_imports = \"silent\"\nignore_missing_imports = true\ndisallow_incomplete_defs = true\ndisallow_untyped_defs = true\ndisallow_untyped_decorators = true\nexclude = \"pdm/(pep582/|models/in_process/.+\\\\.py|misc)\"\nnamespace_packages = true\nmypy_path = \"src\"\nexplicit_package_bases = true\n\n[tool.pdm.version]\nsource = \"scm\"\nwrite_to = \"pdm/VERSION\"\n\n[tool.pdm.build]\nexcludes = [\"./**/.git\"]\npackage-dir = \"src\"\nincludes = [\"src/pdm\"]\nsource-includes = [\"tests\", \"typings\", \"CHANGELOG.md\", \"LICENSE\", \"README.md\", \"tox.ini\"]\n# editables backend doesn't work well with namespace packages\neditable-backend = \"path\"\nlocked = true\nlocked-groups = [\"default\", \"all\"]\n\n[tool.pdm.scripts]\npre_release = \"python tasks/max_versions.py\"\nrelease = \"python tasks/release.py\"\ntest = \"pytest\"\ncoverage = {shell = \"\"\"\\\n                    python -m pytest \\\n                              --verbosity=3 \\\n                              --cov=src/pdm \\\n                              --cov-branch \\\n                              --cov-report term-missing \\\n                              tests/\n                    \"\"\"}\ntox = \"tox\"\ndoc = {cmd = \"mkdocs serve\", help = \"Start the dev server for docs preview\"}\nlint = \"prek run --all-files\"\ncomplete = {call = \"tasks.complete:main\", help = \"Create autocomplete files for bash and fish\"}\n"
  },
  {
    "path": "src/pdm/__init__.py",
    "content": "import pkgutil\n\n__path__ = pkgutil.extend_path(__path__, __name__)\n"
  },
  {
    "path": "src/pdm/__main__.py",
    "content": "from pdm.core import main\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "src/pdm/__version__.py",
    "content": "from pdm.compat import importlib_metadata, resources_read_text\n\n\ndef read_version() -> str:\n    try:\n        return importlib_metadata.version(__package__ or \"pdm\")\n    except importlib_metadata.PackageNotFoundError:\n        return resources_read_text(\"pdm\", \"VERSION\").strip()\n\n\n__version__ = read_version()\n"
  },
  {
    "path": "src/pdm/_types.py",
    "content": "from __future__ import annotations\n\nimport dataclasses as dc\nimport re\nfrom typing import TYPE_CHECKING, Any, NamedTuple, TypeVar, Union\n\nif TYPE_CHECKING:\n    from typing import Protocol\n\n\ndef _normalize_pattern(pattern: str) -> str:\n    return re.sub(r\"[^A-Za-z0-9?*\\[\\]-]+\", \"-\", pattern).lower()\n\n\n@dc.dataclass\nclass RepositoryConfig:\n    \"\"\"Private dataclass to be subclassed\"\"\"\n\n    config_prefix: str\n    name: str\n\n    url: str | None = None\n    username: str | None = None\n    password: str | None = dc.field(default=None, repr=False)\n    verify_ssl: bool | None = None\n    type: str | None = None\n    ca_certs: str | None = None\n    client_cert: str | None = None\n    client_key: str | None = None\n    include_packages: list[str] = dc.field(default_factory=list)\n    exclude_packages: list[str] = dc.field(default_factory=list)\n\n    def __post_init__(self) -> None:\n        self.include_packages = [_normalize_pattern(p) for p in self.include_packages]\n        self.exclude_packages = [_normalize_pattern(p) for p in self.exclude_packages]\n\n    def populate_keyring_auth(self) -> None:\n        if self.username is None or self.password is None:\n            from pdm.models.auth import keyring\n\n            service = f\"pdm-{self.config_prefix}-{self.name}\"\n            auth = keyring.get_auth_info(service, self.username)\n            if auth is not None:\n                self.username, self.password = auth\n\n    def passive_update(self, other: RepositoryConfig | None = None, **kwargs: Any) -> None:\n        \"\"\"An update method that prefers the existing value over the new one.\"\"\"\n        if other is not None:\n            for k in other.__dataclass_fields__:\n                v = getattr(other, k)\n                if getattr(self, k) is None and v is not None:\n                    setattr(self, k, v)\n        for k, v in kwargs.items():\n            if getattr(self, k) is None and v is not None:\n                setattr(self, k, v)\n\n    def __rich__(self) -> str:\n        config_prefix = f\"{self.config_prefix}.{self.name}.\" if self.name else f\"{self.config_prefix}.\"\n        lines: list[str] = []\n        self.populate_keyring_auth()\n        if self.url:\n            lines.append(f\"[primary]{config_prefix}url[/] = {self.url}\")\n        if self.username:\n            lines.append(f\"[primary]{config_prefix}username[/] = {self.username}\")\n        if self.password:\n            lines.append(f\"[primary]{config_prefix}password[/] = [i]<hidden>[/]\")\n        if self.verify_ssl is not None:\n            lines.append(f\"[primary]{config_prefix}verify_ssl[/] = {self.verify_ssl}\")\n        if self.type:\n            lines.append(f\"[primary]{config_prefix}type[/] = {self.type}\")\n        if self.ca_certs:\n            lines.append(f\"[primary]{config_prefix}ca_certs[/] = {self.ca_certs}\")\n        return \"\\n\".join(lines)\n\n    @property\n    def url_with_credentials(self) -> HiddenText:\n        from urllib.parse import urlsplit, urlunsplit\n\n        from pdm.utils import expand_env_vars_in_auth, hide_url\n\n        assert self.url is not None\n        self.populate_keyring_auth()\n        if not self.username or not self.password:\n            return hide_url(expand_env_vars_in_auth(self.url))\n        parsed = urlsplit(self.url)\n        *_, netloc = parsed.netloc.rpartition(\"@\")\n        netloc = f\"{self.username}:{self.password}@{netloc}\"\n        url = urlunsplit((parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment))\n        return hide_url(url)\n\n\nRequirementDict = Union[str, dict[str, Union[str, bool]]]\nCandidateInfo = tuple[list[str], str, str]\n\n\nclass SearchResult(NamedTuple):\n    name: str\n    version: str\n    summary: str\n\n\nSearchResults = list[SearchResult]\n\n\nif TYPE_CHECKING:\n    from typing import Required, TypedDict\n\n    class Comparable(Protocol):\n        def __lt__(self, __other: Any) -> bool: ...\n\n    SpinnerT = TypeVar(\"SpinnerT\", bound=\"Spinner\")\n\n    class Spinner(Protocol):\n        def update(self, text: str) -> None: ...\n\n        def __enter__(self: SpinnerT) -> SpinnerT: ...\n\n        def __exit__(self, *args: Any) -> None: ...\n\n    class RichProtocol(Protocol):\n        def __rich__(self) -> str: ...\n\n    class FileHash(TypedDict, total=False):\n        url: str\n        hash: Required[str]\n        file: str\n\n\nclass NotSetType:\n    pass\n\n\nNotSet = NotSetType()\n\n\n@dc.dataclass(frozen=True)\nclass HiddenText:\n    secret: str\n    redacted: str = dc.field(compare=False, hash=False)\n\n    def __str__(self) -> str:\n        return self.redacted\n\n    def __repr__(self) -> str:\n        return repr(str(self))\n"
  },
  {
    "path": "src/pdm/builders/__init__.py",
    "content": "from pdm.builders.editable import EditableBuilder\nfrom pdm.builders.sdist import SdistBuilder\nfrom pdm.builders.wheel import WheelBuilder\n\n__all__ = [\"EditableBuilder\", \"SdistBuilder\", \"WheelBuilder\"]\n"
  },
  {
    "path": "src/pdm/builders/base.py",
    "content": "from __future__ import annotations\n\nimport functools\nimport logging\nimport os\nimport shutil\nimport subprocess\nimport textwrap\nimport threading\nfrom logging import Logger\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, ClassVar, Iterable, cast\n\nfrom pyproject_hooks import (\n    BackendInvalid,\n    BackendUnavailable,\n    BuildBackendHookCaller,\n    HookMissing,\n    UnsupportedOperation,\n)\n\nfrom pdm.compat import tomllib\nfrom pdm.environments import PythonEnvironment\nfrom pdm.exceptions import BuildError\nfrom pdm.models.in_process import get_sys_config_paths\nfrom pdm.models.requirements import Requirement, parse_requirement\nfrom pdm.models.working_set import WorkingSet\nfrom pdm.termui import logger\n\nif TYPE_CHECKING:\n    from typing import Callable, ParamSpec, TypeVar\n\n    from pdm.environments import BaseEnvironment\n\n    R = TypeVar(\"R\")\n    P = ParamSpec(\"P\")\n\n\nclass LoggerWrapper(threading.Thread):\n    \"\"\"\n    Read messages from a pipe and redirect them\n    to a logger (see python's logging module).\n    \"\"\"\n\n    def __init__(self, logger: Logger, level: int) -> None:\n        super().__init__()\n        self.daemon = True\n\n        self.logger = logger\n        self.level = level\n\n        # create the pipe and reader\n        self.fd_read, self.fd_write = os.pipe()\n\n        self.start()\n        self._output_buffer: list[str] = []\n\n    def fileno(self) -> int:\n        return self.fd_write\n\n    @staticmethod\n    def remove_newline(msg: str) -> str:\n        return msg[:-1] if msg.endswith(\"\\n\") else msg\n\n    def run(self) -> None:\n        with os.fdopen(self.fd_read, encoding=\"utf-8\", errors=\"replace\") as reader:\n            for line in reader:\n                self._write(self.remove_newline(line))\n\n    def _write(self, message: str) -> None:\n        self.logger.log(self.level, message)\n        self._output_buffer.append(message)\n        if len(self._output_buffer) > 10:\n            self._output_buffer[:-10] = []\n\n    def stop(self) -> None:\n        os.close(self.fd_write)\n        self.join()\n\n\ndef wrap_error(func: Callable[P, R]) -> Callable[P, R]:\n    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:\n        try:\n            return func(*args, **kwargs)\n        except (HookMissing, BackendUnavailable, BackendInvalid, UnsupportedOperation) as e:\n            raise BuildError(str(e)) from e\n\n    return functools.update_wrapper(wrapper, func)\n\n\ndef build_error(e: subprocess.CalledProcessError) -> BuildError:\n    \"\"\"Get a build error with meaningful error message\n    from the subprocess output.\n    \"\"\"\n    output = cast(\"list[str]\", e.output)\n    errors: list[str] = []\n    if output and output[-1].strip().startswith(\"ModuleNotFoundError\"):\n        package = output[-1].strip().split()[-1]\n        errors.append(\n            f\"Module {package} is missing, please make sure it is specified in the \"\n            \"'build-system.requires' section. If it is not possible, \"\n            \"add it to the project and use '--no-isolation' option.\"\n        )\n    errors.extend([\"Showing the last 10 lines of the build output:\", *output])\n    error_message = \"\\n\".join(errors)\n    return BuildError(f\"Build backend raised error: {error_message}\")\n\n\ndef log_subprocessor(\n    cmd: list[str],\n    cwd: str | Path | None = None,\n    extra_environ: dict[str, str] | None = None,\n) -> None:\n    env = os.environ.copy()\n    if extra_environ:\n        env.update(extra_environ)\n    outstream = LoggerWrapper(logger, logging.INFO)\n    try:\n        subprocess.check_call(\n            cmd,\n            cwd=cwd,\n            env=env,\n            stdout=outstream.fileno(),\n            stderr=subprocess.STDOUT,\n        )\n    except subprocess.CalledProcessError as e:\n        e.output = outstream._output_buffer\n        raise build_error(e) from None\n    finally:\n        outstream.stop()\n\n\nclass _Prefix:\n    def __init__(self, executable: str, shared: str, overlay: str) -> None:\n        self.bin_dirs: list[str] = []\n        self.lib_dirs: list[str] = []\n        for path in (overlay, shared):\n            paths = get_sys_config_paths(executable, vars={\"base\": path, \"platbase\": path}, kind=\"prefix\")\n            self.bin_dirs.append(paths[\"scripts\"])\n            self.lib_dirs.extend({paths[\"platlib\"], paths[\"purelib\"]})\n        self.site_dir = os.path.join(overlay, \"site\")\n        if os.path.isdir(self.site_dir):\n            # Clear existing site dir as .pyc may be cached.\n            shutil.rmtree(self.site_dir)\n        os.makedirs(self.site_dir)\n        with open(os.path.join(self.site_dir, \"sitecustomize.py\"), \"w\", encoding=\"utf-8\") as fp:\n            fp.write(\n                textwrap.dedent(\n                    f\"\"\"\n                import sys, os, site\n\n                original_sys_path = sys.path[:]\n                known_paths = set()\n                site.addusersitepackages(known_paths)\n                site.addsitepackages(known_paths)\n                known_paths = {{os.path.normcase(p) for p in known_paths}}\n                original_sys_path = [\n                    p for p in original_sys_path\n                    if os.path.normcase(p) not in known_paths]\n                sys.path[:] = original_sys_path\n                for lib_path in {self.lib_dirs!r}:\n                    site.addsitedir(lib_path)\n                \"\"\"\n                )\n            )\n        self.shared = shared\n        self.overlay = overlay\n\n\nclass EnvBuilder:\n    \"\"\"A simple PEP 517 builder for an isolated environment\"\"\"\n\n    DEFAULT_BACKEND: ClassVar[dict[str, Any]] = {\n        \"build-backend\": \"setuptools.build_meta:__legacy__\",\n        \"requires\": [\"setuptools>=61\"],\n    }\n\n    _shared_envs: ClassVar[dict[int, str]] = {}\n    _overlay_envs: ClassVar[dict[str, str]] = {}\n\n    if TYPE_CHECKING:\n        _hook: BuildBackendHookCaller\n        _requires: list[str]\n        _prefix: _Prefix | None\n\n    def get_shared_env(self, key: int) -> str:\n        if key in self._shared_envs:\n            logger.debug(\"Reusing shared build env: %s\", self._shared_envs[key])\n            return self._shared_envs[key]\n        # We don't save the cache here, instead it will be done after the installation\n        # finished.\n        return self._env.project.core.create_temp_dir(\"-shared\", \"pdm-build-env-\")\n\n    def get_overlay_env(self, key: str) -> str:\n        if key not in self._overlay_envs:\n            self._overlay_envs[key] = self._env.project.core.create_temp_dir(\"-overlay\", \"pdm-build-env-\")\n        return self._overlay_envs[key]\n\n    def __init__(self, src_dir: str | Path, environment: BaseEnvironment) -> None:\n        \"\"\"If isolated is True(default), the builder will set up a *clean* environment.\n        Otherwise, the environment of the host Python will be used.\n        \"\"\"\n        self._env = environment\n        self.executable = self._env.interpreter.executable.as_posix()\n        self.src_dir = src_dir\n        self.isolated = environment.project.core.state.build_isolation\n        self.config_settings = environment.project.core.state.config_settings\n        mode = \"Isolated\" if self.isolated else \"Non-isolated\"\n        logger.info(\"Preparing environment(%s mode) for PEP 517 build...\", mode)\n        try:\n            with open(os.path.join(src_dir, \"pyproject.toml\"), \"rb\") as f:\n                spec = tomllib.load(f)\n        except FileNotFoundError:\n            spec = {}\n        except Exception as e:\n            raise BuildError(e) from e\n        build_system = spec.get(\"build-system\", self.DEFAULT_BACKEND)\n\n        if \"build-backend\" not in build_system:\n            build_system[\"build-backend\"] = self.DEFAULT_BACKEND[\"build-backend\"]\n\n        if \"requires\" not in build_system:\n            raise BuildError(\"Missing 'build-system.requires' in pyproject.toml\")\n\n        self.init_build_system(build_system)\n\n    def init_build_system(self, build_system: dict[str, Any]) -> None:\n        \"\"\"Initialize the build system and requires list from the PEP 517 spec\"\"\"\n        self._hook = BuildBackendHookCaller(\n            self.src_dir,\n            build_system[\"build-backend\"],\n            backend_path=build_system.get(\"backend-path\"),\n            runner=self.subprocess_runner,\n            python_executable=self.executable,\n        )\n        self._requires = build_system[\"requires\"]\n        self._prefix = (\n            _Prefix(\n                self.executable,\n                # Build backends with the same requires list share the cached base env.\n                shared=self.get_shared_env(hash(frozenset(self._requires))),\n                # Overlay envs are unique for each source to be built.\n                overlay=self.get_overlay_env(os.path.normcase(self.src_dir).rstrip(\"\\\\/\")),\n            )\n            if self.isolated\n            else None\n        )\n\n    @property\n    def _env_vars(self) -> dict[str, str]:\n        env: dict[str, str] = {}\n        if self.isolated:\n            assert self._prefix is not None\n            paths = self._prefix.bin_dirs[:]\n            env.update(\n                {\n                    \"PYTHONPATH\": self._prefix.site_dir,\n                    \"PYTHONNOUSERSITE\": \"1\",\n                }\n            )\n        else:\n            env_paths = self._env.get_paths()\n            pythonpath = list({env_paths[\"purelib\"], env_paths[\"platlib\"]})\n            if \"PYTHONPATH\" in os.environ:\n                pythonpath.append(os.getenv(\"PYTHONPATH\", \"\"))\n            env.update(\n                PYTHONPATH=os.pathsep.join(pythonpath),\n            )\n            paths = [env_paths[\"scripts\"]]\n        if \"PATH\" in os.environ:\n            paths.append(os.getenv(\"PATH\", \"\"))\n        env[\"PATH\"] = os.pathsep.join(paths)\n        return env\n\n    def subprocess_runner(\n        self, cmd: list[str], cwd: str | Path | None = None, extra_environ: dict[str, str] | None = None\n    ) -> None:\n        env = self._env_vars.copy()\n        if extra_environ:\n            env.update(extra_environ)\n        return log_subprocessor(cmd, cwd, extra_environ=env)\n\n    def check_requirements(self, reqs: Iterable[str]) -> Iterable[Requirement]:\n        missing = set()\n        conflicting = set()\n        env_paths = self._env.get_paths()\n        libs = (\n            list({env_paths[\"purelib\"], env_paths[\"platlib\"]})\n            if not self.isolated\n            else cast(_Prefix, self._prefix).lib_dirs\n        )\n        if reqs:\n            ws = WorkingSet(libs)\n            for req in reqs:\n                parsed_req = parse_requirement(req)\n                parsed_req.groups = [\"default\"]\n                if parsed_req.marker and not parsed_req.marker.matches(self._env.spec):\n                    logger.debug(\n                        \"Skipping requirement %s: mismatching marker %s\",\n                        req,\n                        parsed_req.marker,\n                    )\n                    continue\n                if parsed_req.identify() not in ws:\n                    missing.add(parsed_req)\n                elif parsed_req.specifier and not parsed_req.specifier.contains(\n                    ws[parsed_req.identify()].version, prereleases=True\n                ):\n                    conflicting.add(req)\n        if conflicting:\n            raise BuildError(f\"Conflicting requirements: {', '.join(conflicting)}\")\n        return missing\n\n    def install(self, requirements: Iterable[str], shared: bool = False) -> None:\n        from pdm.installers.core import install_requirements\n\n        missing = list(self.check_requirements(requirements))\n        if not missing:\n            return\n        assert self._prefix is not None\n        path = self._prefix.shared if shared else self._prefix.overlay\n        env = PythonEnvironment(self._env.project, python=str(self._env.interpreter.path), prefix=path)\n        install_requirements(missing, env, allow_uv=False)\n\n        if shared:\n            # The shared env is prepared and is safe to be cached now. This is to make\n            # sure no broken env is returned early when run in parallel mode.\n            key = hash(frozenset(requirements))\n            if key not in self._shared_envs:\n                self._shared_envs[key] = path\n\n    def prepare_metadata(self, out_dir: str) -> str:\n        \"\"\"Prepare metadata and store in the out_dir.\n        Some backends doesn't provide that API, in that case the metadata will be\n        retrieved from the built result.\n        \"\"\"\n        raise NotImplementedError(\"Should be implemented in subclass\")\n\n    def build(self, out_dir: str, metadata_directory: str | None = None) -> str:\n        \"\"\"Build and store the artifact in out_dir,\n        return the absolute path of the built result.\n        \"\"\"\n        raise NotImplementedError(\"Should be implemented in subclass\")\n"
  },
  {
    "path": "src/pdm/builders/editable.py",
    "content": "from __future__ import annotations\n\nimport os\n\nfrom pdm.builders.base import EnvBuilder, wrap_error\n\n\nclass EditableBuilder(EnvBuilder):\n    \"\"\"Build egg-info in isolated env with managed Python.\"\"\"\n\n    @wrap_error\n    def prepare_metadata(self, out_dir: str) -> str:\n        if self.isolated:\n            self.install(self._requires, shared=True)\n            requires = self._hook.get_requires_for_build_editable(self.config_settings)\n            self.install(requires)\n        filename = self._hook.prepare_metadata_for_build_editable(out_dir, self.config_settings)\n        return os.path.join(out_dir, filename)\n\n    @wrap_error\n    def build(self, out_dir: str, metadata_directory: str | None = None) -> str:\n        if self.isolated:\n            self.install(self._requires, shared=True)\n            requires = self._hook.get_requires_for_build_editable(self.config_settings)\n            self.install(requires)\n        filename = self._hook.build_editable(out_dir, self.config_settings, metadata_directory)\n        return os.path.join(out_dir, filename)\n"
  },
  {
    "path": "src/pdm/builders/sdist.py",
    "content": "from __future__ import annotations\n\nimport os\n\nfrom pdm.builders.base import EnvBuilder, wrap_error\n\n\nclass SdistBuilder(EnvBuilder):\n    \"\"\"Build sdist in isolated env with managed Python.\"\"\"\n\n    @wrap_error\n    def build(self, out_dir: str, metadata_directory: str | None = None) -> str:\n        if self.isolated:\n            self.install(self._requires, shared=True)\n            requires = self._hook.get_requires_for_build_sdist(self.config_settings)\n            self.install(requires)\n        filename = self._hook.build_sdist(out_dir, self.config_settings)\n        return os.path.join(out_dir, filename)\n"
  },
  {
    "path": "src/pdm/builders/wheel.py",
    "content": "from __future__ import annotations\n\nimport os\n\nfrom pdm.builders.base import EnvBuilder, wrap_error\n\n\nclass WheelBuilder(EnvBuilder):\n    \"\"\"Build wheel in isolated env with managed Python.\"\"\"\n\n    @wrap_error\n    def prepare_metadata(self, out_dir: str) -> str:\n        if self.isolated:\n            self.install(self._requires, shared=True)\n            requires = self._hook.get_requires_for_build_wheel(self.config_settings)\n            self.install(requires)\n        filename = self._hook.prepare_metadata_for_build_wheel(out_dir, self.config_settings)\n        return os.path.join(out_dir, filename)\n\n    @wrap_error\n    def build(self, out_dir: str, metadata_directory: str | None = None) -> str:\n        if self.isolated:\n            self.install(self._requires, shared=True)\n            requires = self._hook.get_requires_for_build_wheel(self.config_settings)\n            self.install(requires)\n        filename = self._hook.build_wheel(out_dir, self.config_settings, metadata_directory)\n        return os.path.join(out_dir, filename)\n"
  },
  {
    "path": "src/pdm/cli/__init__.py",
    "content": ""
  },
  {
    "path": "src/pdm/cli/actions.py",
    "content": "from __future__ import annotations\n\nimport contextlib\nimport datetime\nimport hashlib\nimport inspect\nimport json\nimport os\nimport sys\nimport textwrap\nfrom typing import TYPE_CHECKING, Collection, Iterable, cast\n\nfrom resolvelib.resolvers import ResolutionImpossible, ResolutionTooDeep\n\nfrom pdm import termui\nfrom pdm.cli.filters import GroupSelection\nfrom pdm.cli.hooks import HookManager\nfrom pdm.cli.utils import (\n    check_project_file,\n    find_importable_files,\n    format_resolution_impossible,\n    get_pep582_path,\n    set_env_in_reg,\n)\nfrom pdm.environments import BareEnvironment\nfrom pdm.exceptions import PdmException, PdmUsageError, ProjectError, ResolutionError\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.markers import EnvSpec\nfrom pdm.models.repositories import LockedRepository, Package\nfrom pdm.project import Project\nfrom pdm.project.lockfile import FLAG_CROSS_PLATFORM, FLAG_INHERIT_METADATA, FLAG_STATIC_URLS\nfrom pdm.resolver.reporters import RichLockReporter\nfrom pdm.termui import logger\nfrom pdm.utils import deprecation_warning\n\nif TYPE_CHECKING:\n    from pdm.models.requirements import Requirement\n\n\ndef do_lock(\n    project: Project,\n    strategy: str = \"all\",\n    tracked_names: Iterable[str] | None = None,\n    requirements: list[Requirement] | None = None,\n    dry_run: bool = False,\n    refresh: bool = False,\n    groups: list[str] | None = None,\n    strategy_change: list[str] | None = None,\n    hooks: HookManager | None = None,\n    env_spec: EnvSpec | None = None,\n    append: bool = False,\n) -> dict[str, list[Candidate]]:\n    \"\"\"Performs the locking process and update lockfile.\"\"\"\n    hooks = hooks or HookManager(project)\n    check_project_file(project)\n    lock_strategy = project.lockfile.apply_strategy_change(strategy_change or [])\n    if FLAG_CROSS_PLATFORM in lock_strategy:  # pragma: no cover\n        project.core.ui.deprecated(\n            \"`cross_platform` strategy is deprecated in favor of the new lock targets.\\n\"\n            \"See docs: http://pdm-project.org/en/latest/usage/lock-targets/\"\n        )\n    if strategy_change and append:\n        raise PdmUsageError(\"Not allowed to change lock strategy when --append is used.\")\n    locked_repo = project.get_locked_repository()\n    candidates = [entry.candidate for entry in locked_repo.packages.values()]\n    if refresh or (env_spec is not None and not append):\n        # Refetch hashes if --platform/--python/--implementation/--refresh is used\n        for c in candidates:\n            c.hashes.clear()\n    if refresh:\n        if env_spec is not None:\n            raise PdmUsageError(\"Cannot pass --python/--platform/--implementation with --refresh\")\n        repo = project.get_repository()\n        with project.core.ui.open_spinner(\"Re-calculating hashes...\"):\n            with project.core.ui.logging(\"lock\"):\n                repo.fetch_hashes(candidates)\n            project.lockfile.format_lockfile(locked_repo, groups=project.lockfile.groups, strategy=lock_strategy)\n        project.write_lockfile()\n        return locked_repo.all_candidates\n\n    if groups is None:\n        groups = list(project.iter_groups())\n    if not requirements:\n        all_deps = project._resolve_dependencies(groups)\n        requirements = [r for group in groups for r in project.get_dependencies(group, all_deps)]\n    ui = project.core.ui\n    supports_env_spec = \"env_spec\" in inspect.signature(project.get_provider).parameters\n    # The repository to store the lock result\n    if locked_repo.packages:\n        result_repo = LockedRepository({}, sources=project.sources, environment=project.environment)\n    else:\n        # Use the same repository if the lock is empty.\n        result_repo = locked_repo\n    if not supports_env_spec:  # pragma: no cover\n        ui.warn(\"Lock targets are not supported by the current provider\")\n\n    if append:\n        if env_spec is None:\n            raise PdmUsageError(\"Cannot use `--append` without --python/--platform/--implementation\")\n        if env_spec in locked_repo.targets:\n            ui.echo(f\"{termui.Emoji.LOCK} Lock target {env_spec} already exists, skip locking.\")\n            return locked_repo.all_candidates\n        targets = [env_spec]\n        result_repo = locked_repo  # Append to the same lockfile\n    else:\n        targets = [env_spec] if env_spec else (locked_repo.targets[:] or project.lock_targets)\n    # Restrict the target python to within the project's requires-python\n    global_requires_python = project.environment.python_requires\n    for i, target in enumerate(targets):\n        if (merged := global_requires_python & target.requires_python).is_empty():\n            raise PdmUsageError(\n                f\"The target requires Python {target.requires_python} which is not compatible with \"\n                f\"the project's requires-python {global_requires_python}\"\n            )\n        targets[i] = target.replace(requires_python=merged)\n    resolve_max_rounds = int(project.config[\"strategy.resolve_max_rounds\"])\n    hooks.try_emit(\"pre_lock\", requirements=requirements, dry_run=dry_run)\n    with ui.logging(\"lock\"):\n        for req in list(requirements):\n            if req.key == \"python\":\n                requirements.remove(req)\n                logger.warning(\"The 'python' requirement is not necessary and will be ignored.\")\n        # The context managers are nested to ensure the spinner is stopped before\n        # any message is thrown to the output.\n        resolver_class = project.get_resolver()\n        with RichLockReporter(requirements, ui) as reporter:\n            collected_groups: set[str] = set()\n            try:\n                for target in targets:\n                    resolver = resolver_class(\n                        environment=project.environment,\n                        requirements=[r for r in requirements if not r.marker or r.marker.matches(target)],\n                        update_strategy=strategy,\n                        strategies=lock_strategy,\n                        target=target,\n                        tracked_names=list(tracked_names or ()),\n                        locked_repository=locked_repo,\n                        reporter=reporter,\n                    )\n                    reporter.update(f\"Resolve for environment {target}\")\n                    resolved, new_groups = resolver.resolve()\n                    collected_groups.update(new_groups)\n                    locked_repo.merge_result(target, resolved)\n                    if result_repo is not locked_repo:\n                        result_repo.merge_result(target, resolved)\n            except ResolutionTooDeep:\n                reporter.update(f\"{termui.Emoji.LOCK} Lock failed.\", info=\"\", completed=1)\n                ui.echo(\n                    \"The dependency resolution exceeds the maximum loop depth of \"\n                    f\"{resolve_max_rounds}, there may be some circular dependencies \"\n                    \"in your project. Try to solve them or increase the \"\n                    f\"[success]`strategy.resolve_max_rounds`[/] config.\",\n                    err=True,\n                )\n                raise ResolutionError(\n                    f\"The dependency resolution exceeds the maximum loop depth of {resolve_max_rounds}\"\n                ) from None\n            except ResolutionImpossible as err:\n                reporter.update(f\"{termui.Emoji.LOCK} Lock failed.\", info=\"\", completed=1)\n                ui.error(format_resolution_impossible(err))\n                raise ResolutionError(\"Unable to find a resolution\") from None\n            else:\n                groups = list(set(groups) | collected_groups)\n                if project.enable_write_lockfile:\n                    reporter.update(f\"{termui.Emoji.LOCK} Lock successful.\", info=\"\", completed=1)\n                project.lockfile.format_lockfile(result_repo, groups=groups, strategy=lock_strategy)\n                project.write_lockfile(write=not dry_run)\n                hooks.try_emit(\"post_lock\", resolution=result_repo.all_candidates, dry_run=dry_run)\n\n    return result_repo.all_candidates\n\n\ndef resolve_from_lockfile(\n    project: Project,\n    requirements: Iterable[Requirement],\n    groups: Collection[str] | None = None,\n    env_spec: EnvSpec | None = None,\n) -> Iterable[Package]:\n    from dep_logic.tags import EnvCompatibility\n\n    from pdm.resolver.resolvelib import RLResolver\n\n    ui = project.core.ui\n\n    if env_spec is None:\n        # Resolve for the current environment by default\n        env_spec = project.environment.spec\n    reqs = [req for req in requirements if not req.marker or req.marker.matches(env_spec)]\n    with ui.open_spinner(\"Resolving packages from lockfile...\"):\n        locked_repo = project.get_locked_repository(env_spec)\n        lock_targets = locked_repo.targets\n        if env_spec not in lock_targets:\n            compatibilities = [target.compare(env_spec) for target in lock_targets]\n            if not any(compat == EnvCompatibility.LOWER_OR_EQUAL for compat in compatibilities):\n                loose_compatible_target = next(\n                    (\n                        target\n                        for (target, compat) in zip(lock_targets, compatibilities)\n                        if compat == EnvCompatibility.HIGHER\n                    ),\n                    None,\n                )\n                if loose_compatible_target is not None:\n                    ui.warn(f\"Found lock target {loose_compatible_target}, installing for env {env_spec}\")\n                elif not lock_targets:  # pragma: no cover\n                    ui.warn(\"Missing lock targets or environment field in the lock file, installing it anyway.\")\n                else:\n                    errors = [f\"None of the lock targets matches the current env {env_spec}:\"] + [\n                        f\" - {target}\" for target in lock_targets\n                    ]\n                    ui.error(\"\\n\".join(errors))\n                    raise PdmException(\"No compatible lock target found\")\n\n        with ui.logging(\"install-resolve\"):\n            strategies = project.lockfile.strategy.copy()\n            if FLAG_INHERIT_METADATA in strategies and groups is not None and not project.config[\"use_uv\"]:\n                return locked_repo.evaluate_candidates(groups)\n            strategies.add(FLAG_STATIC_URLS)\n            resolver = project.get_resolver()(\n                environment=project.environment,\n                requirements=reqs,\n                update_strategy=\"reuse\",\n                strategies=strategies,\n                target=env_spec,\n                tracked_names=(),\n                locked_repository=locked_repo,\n            )\n            if isinstance(resolver, RLResolver):  # resolve from lock file\n                resolver.provider.repository = locked_repo\n            try:\n                return resolver.resolve().packages\n            except ResolutionImpossible as e:\n                logger.exception(\"Broken lockfile\")\n                raise PdmException(\n                    \"Resolving from lockfile failed. You may fix the lockfile by `pdm lock --update-reuse` and retry.\"\n                ) from e\n\n\ndef resolve_candidates_from_lockfile(\n    project: Project,\n    requirements: Iterable[Requirement],\n    cross_platform: bool | None = None,\n    groups: Collection[str] | None = None,\n    env_spec: EnvSpec | None = None,\n) -> dict[str, Candidate]:\n    if cross_platform is not None:  # pragma: no cover\n        deprecation_warning(\"cross_platform argument is deprecated\", stacklevel=2)\n    packages = resolve_from_lockfile(project, requirements, groups, env_spec)\n    return {p.candidate.identify(): p.candidate for p in packages}\n\n\ndef check_lockfile(project: Project, raise_not_exist: bool = True) -> str | None:\n    \"\"\"Check if the lock file exists and is up to date. Return the lock strategy.\"\"\"\n    from pdm.project.lockfile.base import Compatibility\n\n    if not project.lockfile.exists():\n        if raise_not_exist:\n            raise ProjectError(\"Lockfile does not exist, nothing to install\")\n        project.core.ui.warn(\"Lockfile does not exist\")\n        return \"all\"\n    compat = project.lockfile.compatibility()\n    if compat == Compatibility.NONE:\n        project.core.ui.warn(\"Lockfile is not compatible with PDM\")\n        return \"reuse\"\n    elif compat == Compatibility.BACKWARD:\n        project.core.ui.warn(\"Lockfile is generated on an older version of PDM\")\n    elif compat == Compatibility.FORWARD:\n        project.core.ui.warn(\"Lockfile is generated on a newer version of PDM\")\n    if not project.is_lockfile_hash_match():\n        project.core.ui.warn(\"Lockfile hash doesn't match pyproject.toml, packages may be outdated\")\n        return \"reuse\"\n    return None\n\n\ndef do_sync(\n    project: Project,\n    *,\n    selection: GroupSelection,\n    dry_run: bool = False,\n    clean: bool = False,\n    quiet: bool = False,\n    requirements: list[Requirement] | None = None,\n    tracked_names: Collection[str] | None = None,\n    no_editable: bool | Collection[str] = False,\n    no_self: bool = False,\n    reinstall: bool = False,\n    only_keep: bool = False,\n    fail_fast: bool = False,\n    hooks: HookManager | None = None,\n) -> None:\n    \"\"\"Synchronize project\"\"\"\n    hooks = hooks or HookManager(project)\n    if requirements is None:\n        requirements = []\n        selection.validate()\n        all_deps = project._resolve_dependencies(list(selection))\n        for group in selection:\n            requirements.extend(project.get_dependencies(group, all_deps))\n    packages = list(resolve_from_lockfile(project, requirements, groups=list(selection)))\n    if tracked_names and dry_run:\n        packages = [p for p in packages if p.candidate.identify() in tracked_names]\n    synchronizer = project.get_synchronizer(quiet=quiet)(\n        project.environment,\n        clean=clean,\n        dry_run=dry_run,\n        no_editable=no_editable,\n        install_self=not no_self and project.is_distribution,\n        reinstall=reinstall,\n        only_keep=only_keep,\n        fail_fast=fail_fast,\n        packages=packages,\n    )\n    with project.core.ui.logging(\"install\"):\n        hooks.try_emit(\"pre_install\", packages=packages, dry_run=dry_run)\n        synchronizer.synchronize()\n        hooks.try_emit(\"post_install\", packages=packages, dry_run=dry_run)\n\n\ndef ask_for_import(project: Project) -> None:\n    \"\"\"Show possible importable files and ask user to decide\"\"\"\n    from pdm.cli.commands.import_cmd import Command as ImportCommand\n\n    importable_files = list(find_importable_files(project))\n    if not importable_files:\n        return\n    project.core.ui.echo(\"Found following files from other formats that you may import:\", style=\"primary\")\n    for i, (key, filepath) in enumerate(importable_files):\n        project.core.ui.echo(f\"{i}. [success]{filepath.as_posix()}[/] ({key})\")\n    project.core.ui.echo(f\"{len(importable_files)}. [warning]don't do anything, I will import later.[/]\")\n    choice = termui.ask(\n        \"Please select\",\n        prompt_type=int,\n        choices=[str(i) for i in range(len(importable_files) + 1)],\n        show_choices=False,\n    )\n    if int(choice) == len(importable_files):\n        return\n    key, filepath = importable_files[int(choice)]\n    ImportCommand.do_import(project, str(filepath), key, reset_backend=False)\n\n\ndef print_pep582_command(project: Project, shell: str = \"AUTO\") -> None:\n    \"\"\"Print the export PYTHONPATH line to be evaluated by the shell.\"\"\"\n    import shellingham\n\n    pep582_path = get_pep582_path(project)\n    ui = project.core.ui\n\n    if os.name == \"nt\":\n        try:\n            set_env_in_reg(\"PYTHONPATH\", pep582_path)\n        except PermissionError:\n            ui.error(\"Permission denied, please run the terminal as administrator.\")\n        ui.info(\"The environment variable has been saved, please restart the session to take effect.\")\n        return\n    lib_path = pep582_path.replace(\"'\", \"\\\\'\")\n    if shell == \"AUTO\":\n        shell = shellingham.detect_shell()[0]\n    shell = shell.lower()\n    if shell in (\"zsh\", \"bash\", \"sh\", \"dash\"):\n        result = textwrap.dedent(\n            f\"\"\"\n            if [ -n \"$PYTHONPATH\" ]; then\n                export PYTHONPATH='{lib_path}':$PYTHONPATH\n            else\n                export PYTHONPATH='{lib_path}'\n            fi\n            \"\"\"\n        ).strip()\n    elif shell == \"fish\":\n        result = f\"set -x PYTHONPATH '{lib_path}' $PYTHONPATH\"\n    elif shell in (\"tcsh\", \"csh\"):\n        result = textwrap.dedent(\n            f\"\"\"\n            if ( $?PYTHONPATH ) then\n                if ( \"$PYTHONPATH\" != \"\" ) then\n                    setenv PYTHONPATH '{lib_path}':$PYTHONPATH\n                else\n                    setenv PYTHONPATH '{lib_path}'\n                endif\n            else\n                setenv PYTHONPATH '{lib_path}'\n            endif\n            \"\"\"\n        ).strip()\n    else:\n        raise PdmUsageError(f\"Unsupported shell: {shell}, please specify another shell via `--pep582 <SHELL>`\")\n    ui.echo(result)\n\n\ndef get_latest_pdm_version_from_pypi(project: Project, prereleases: bool = False) -> str | None:\n    \"\"\"Get the latest version of PDM from PyPI.\"\"\"\n    environment = BareEnvironment(project)\n    with environment.get_finder([project.default_source]) as finder:\n        candidate = finder.find_best_match(\"pdm\", allow_prereleases=prereleases).best\n    return cast(str, candidate.version) if candidate else None\n\n\ndef get_latest_version(project: Project, expire_after: int = 7 * 24 * 3600) -> str | None:  # pragma: no cover\n    \"\"\"Get the latest version of PDM from PyPI, cache for 7 days\"\"\"\n    cache_key = hashlib.sha224(sys.executable.encode()).hexdigest()\n    cache_file = project.cache(\"self-check\") / cache_key\n    state = {}\n    with contextlib.suppress(OSError):\n        state = json.loads(cache_file.read_text())\n    current_time = datetime.datetime.now(datetime.timezone.utc).timestamp()\n    if (last_check := state.get(\"last-check\")) and current_time - last_check < expire_after:\n        return cast(str, state[\"latest-version\"])\n    try:\n        latest_version = get_latest_pdm_version_from_pypi(project)\n    except Exception as e:\n        project.core.ui.warn(f\"Failed to get latest version: {e}\", verbosity=termui.Verbosity.NORMAL)\n        latest_version = None\n    if latest_version is None:\n        return None\n    state.update({\"latest-version\": latest_version, \"last-check\": current_time})\n    with contextlib.suppress(OSError):\n        cache_file.write_text(json.dumps(state))\n    return latest_version\n\n\ndef check_update(project: Project) -> None:  # pragma: no cover\n    \"\"\"Check if there is a new version of PDM available\"\"\"\n    from pdm.cli.utils import is_homebrew_installation, is_pipx_installation, is_scoop_installation\n    from pdm.utils import parse_version\n\n    if project.core.ui.verbosity < termui.Verbosity.NORMAL:\n        return\n\n    this_version = project.core.version\n    latest_version = get_latest_version(project)\n    if latest_version is None or parse_version(this_version) >= parse_version(latest_version):\n        return\n    disable_command = \"pdm config check_update false\"\n\n    is_prerelease = parse_version(latest_version).is_prerelease\n\n    if is_pipx_installation():\n        install_command = f\"pipx upgrade {'--pip-args=--pre ' if is_prerelease else ''}pdm\"\n    elif is_homebrew_installation():\n        install_command = \"brew upgrade pdm\"\n    elif is_scoop_installation():\n        install_command = \"scoop update pdm\"\n    else:\n        install_command = \"pdm self update\" + (\" --pre\" if is_prerelease else \"\")\n        if os.name == \"nt\":\n            # On Windows, the executable can't replace itself, we add the python prefix to the command\n            # A bit ugly but it works\n            install_command = f\"{sys.executable} -m {install_command}\"\n\n    message = [\n        f\"PDM [primary]{this_version}[/]\",\n        f\" is installed, while [primary]{latest_version}[/]\",\n        \" is available.\\n\",\n        f\"Please run [req]`{install_command}`[/]\",\n        \" to upgrade.\\n\",\n        f\"Run [req]`{disable_command}`[/]\",\n        \" to disable the check.\",\n    ]\n    project.core.ui.info(\"\".join(message))\n"
  },
  {
    "path": "src/pdm/cli/commands/__init__.py",
    "content": ""
  },
  {
    "path": "src/pdm/cli/commands/add.py",
    "content": "from __future__ import annotations\n\nimport argparse\nfrom typing import TYPE_CHECKING\n\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.filters import GroupSelection\nfrom pdm.cli.hooks import HookManager\nfrom pdm.cli.options import (\n    dry_run_option,\n    frozen_lockfile_option,\n    install_group,\n    lockfile_option,\n    override_option,\n    packages_group,\n    prerelease_option,\n    save_strategy_group,\n    skip_option,\n    unconstrained_option,\n    update_strategy_group,\n    venv_option,\n)\nfrom pdm.exceptions import PdmUsageError\n\nif TYPE_CHECKING:\n    from typing import Collection\n\n    from pdm.models.requirements import Requirement\n    from pdm.project import Project\n\n\nclass Command(BaseCommand):\n    \"\"\"Add package(s) to pyproject.toml and install them\"\"\"\n\n    arguments = (\n        *BaseCommand.arguments,\n        lockfile_option,\n        frozen_lockfile_option,\n        save_strategy_group,\n        override_option,\n        update_strategy_group,\n        prerelease_option,\n        unconstrained_option,\n        packages_group,\n        install_group,\n        dry_run_option,\n        venv_option,\n        skip_option,\n    )\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\n            \"-d\",\n            \"--dev\",\n            default=False,\n            action=\"store_true\",\n            help=\"Add packages into dev dependencies\",\n        )\n        parser.add_argument(\"-G\", \"--group\", help=\"Specify the target dependency group to add into\")\n        parser.add_argument(\n            \"--no-sync\",\n            dest=\"sync\",\n            default=True,\n            action=\"store_false\",\n            help=\"Only write pyproject.toml and do not sync the working set\",\n        )\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        if options.editables and options.no_editable:\n            raise PdmUsageError(\"`--no-editable` cannot be used with `-e/--editable`\")\n        self.do_add(\n            project,\n            selection=GroupSelection.from_options(project, options),\n            sync=options.sync,\n            save=options.save_strategy or project.config[\"strategy.save\"],\n            strategy=options.update_strategy or project.config[\"strategy.update\"],\n            editables=options.editables,\n            packages=options.packages,\n            unconstrained=options.unconstrained,\n            no_editable=options.no_editable,\n            no_self=options.no_self,\n            dry_run=options.dry_run,\n            prerelease=options.prerelease,\n            fail_fast=options.fail_fast,\n            hooks=HookManager(project, options.skip),\n        )\n\n    @staticmethod\n    def do_add(\n        project: Project,\n        *,\n        selection: GroupSelection,\n        sync: bool = True,\n        save: str = \"compatible\",\n        strategy: str = \"reuse\",\n        editables: Collection[str] = (),\n        packages: Collection[str] = (),\n        unconstrained: bool = False,\n        no_editable: bool = False,\n        no_self: bool = False,\n        dry_run: bool = False,\n        prerelease: bool | None = None,\n        fail_fast: bool = False,\n        hooks: HookManager | None = None,\n    ) -> None:\n        \"\"\"Add packages and install\"\"\"\n        from pdm.cli.actions import do_lock, do_sync\n        from pdm.cli.utils import check_project_file, save_version_specifiers\n        from pdm.models.requirements import parse_requirement\n        from pdm.models.specifiers import get_specifier\n        from pdm.utils import normalize_name\n\n        hooks = hooks or HookManager(project)\n        check_project_file(project)\n        if editables and no_editable:\n            raise PdmUsageError(\"Cannot use --no-editable with editable packages given.\")\n        group = selection.one()\n        tracked_names: set[str] = set()\n        requirements: list[Requirement] = []\n        lock_groups = [\"default\"] if project.lockfile.empty() else project.lockfile.groups\n        if lock_groups is not None and group not in lock_groups:\n            if project.enable_write_lockfile:\n                project.core.ui.info(f\"Adding group [success]{group}[/] to lockfile\")\n            lock_groups.append(group)\n        if group == \"default\" or (\n            not selection.dev and normalize_name(group) not in project.pyproject.dev_dependencies\n        ):\n            if editables:\n                raise PdmUsageError(\"Cannot add editables to the default or optional dependency group\")\n        for r in [parse_requirement(line, True) for line in editables] + [parse_requirement(line) for line in packages]:\n            if project.is_distribution and normalize_name(name := project.name) == r.key and not r.extras:\n                project.core.ui.warn(f\"Package [req]{name}[/] is the project itself.\")\n                continue\n            if r.is_file_or_url:\n                r.relocate(project.backend)  # type: ignore[attr-defined]\n            key = r.identify()\n            tracked_names.add(key)\n            requirements.append(r)\n        if requirements:\n            project.core.ui.echo(\n                f\"Adding {'[bold]global[/] ' if project.is_global else ''}packages to [primary]{group}[/] \"\n                f\"{'dev-' if selection.dev else ''}dependencies: \"\n                + \", \".join(f\"[req]{r.as_line()}[/]\" for r in requirements)\n            )\n        project.add_dependencies(requirements, group, selection.dev or False, write=False)\n        all_dependencies = project.all_dependencies\n        group_deps = all_dependencies[group]\n        for req in group_deps:\n            if req.identify() in tracked_names:\n                req.prerelease = prerelease\n        if unconstrained:\n            if not requirements:\n                raise PdmUsageError(\"--unconstrained requires at least one package\")\n            for req in group_deps:\n                req.specifier = get_specifier(\"\")\n\n        reqs = [r for g, deps in all_dependencies.items() for r in deps if lock_groups is None or g in lock_groups]\n        with hooks.skipping(\"post_lock\"):\n            resolved = do_lock(\n                project,\n                strategy,\n                tracked_names,\n                reqs,\n                dry_run=True,\n                hooks=hooks,\n                groups=lock_groups,\n            )\n\n        # Update dependency specifiers and lockfile hash.\n        deps_to_update = group_deps if unconstrained else requirements\n        save_version_specifiers(deps_to_update, resolved, save)\n        if not dry_run:\n            project.add_dependencies(deps_to_update, group, selection.dev or False)\n            project.write_lockfile(show_message=False)\n        hooks.try_emit(\"post_lock\", resolution=resolved, dry_run=dry_run)\n        if sync:\n            do_sync(\n                project,\n                selection=GroupSelection(project, groups=[group], default=False),\n                no_editable=no_editable and tracked_names,\n                no_self=no_self or group != \"default\",\n                requirements=list(group_deps),\n                dry_run=dry_run,\n                fail_fast=fail_fast,\n                hooks=hooks,\n            )\n"
  },
  {
    "path": "src/pdm/cli/commands/base.py",
    "content": "from __future__ import annotations\n\nimport argparse\nfrom argparse import _SubParsersAction\nfrom typing import Any, Sequence, TypeVar\n\nfrom pdm.cli.options import Option, global_option, project_option, verbose_option\nfrom pdm.project import Project\n\nC = TypeVar(\"C\", bound=\"BaseCommand\")\n\n\nclass BaseCommand:\n    \"\"\"A CLI subcommand\"\"\"\n\n    # The subcommand's name\n    name: str | None = None\n    # The subcommand's help string, if not given, __doc__ will be used.\n    description: str | None = None\n    # A list of pre-defined options which will be loaded on initializing\n    # Rewrite this if you don't want the default ones\n    arguments: Sequence[Option] = (verbose_option, global_option, project_option)\n\n    @classmethod\n    def init_parser(cls: type[C], parser: argparse.ArgumentParser) -> C:\n        cmd = cls()\n        for arg in cmd.arguments:\n            arg.add_to_parser(parser)\n        cmd.add_arguments(parser)\n        return cmd\n\n    @classmethod\n    def register_to(cls, subparsers: _SubParsersAction, name: str | None = None, **kwargs: Any) -> None:\n        \"\"\"Register a subcommand to the subparsers,\n        with an optional name of the subcommand.\n        \"\"\"\n        help_text = cls.description or cls.__doc__\n        name = name or cls.name or \"\"\n        # Remove the existing subparser as it will raise an error on Python 3.11+\n        subparsers._name_parser_map.pop(name, None)\n        subactions = subparsers._get_subactions()\n        subactions[:] = [action for action in subactions if action.dest != name]\n        parser = subparsers.add_parser(\n            name,\n            description=help_text,\n            help=help_text,\n            **kwargs,\n        )\n        command = cls.init_parser(parser)\n        command.name = name\n        # Store the command instance in the parsed args. See pdm/core.py for more details\n        parser.set_defaults(command=command)\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        \"\"\"Manipulate the argument parser to add more arguments\"\"\"\n        pass\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        \"\"\"The command handler function.\n\n        :param project: the pdm project instance\n        :param options: the parsed Namespace object\n        \"\"\"\n        raise NotImplementedError\n"
  },
  {
    "path": "src/pdm/cli/commands/build.py",
    "content": "from __future__ import annotations\n\nimport argparse\nimport os\nimport shutil\nimport tarfile\nimport tempfile\nfrom pathlib import Path\nfrom typing import Mapping\n\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.hooks import HookManager\nfrom pdm.cli.options import (\n    config_setting_option,\n    no_isolation_option,\n    project_option,\n    skip_option,\n    verbose_option,\n)\nfrom pdm.exceptions import ProjectError\nfrom pdm.project import Project\n\n\nclass Command(BaseCommand):\n    \"\"\"Build artifacts for distribution\"\"\"\n\n    arguments = (verbose_option, project_option, no_isolation_option, skip_option, config_setting_option)\n\n    @staticmethod\n    def do_build(\n        project: Project,\n        sdist: bool = True,\n        wheel: bool = True,\n        dest: str = \"dist\",\n        clean: bool = True,\n        verbose: int = 0,\n        config_settings: Mapping[str, str] | None = None,\n        hooks: HookManager | None = None,\n    ) -> None:\n        \"\"\"Build artifacts for distribution.\"\"\"\n        from pdm.builders import SdistBuilder, WheelBuilder\n\n        hooks = hooks or HookManager(project)\n        config_settings = project.core.state.config_settings\n\n        if project.is_global:\n            raise ProjectError(\"Not allowed to build based on the global project.\")\n        if not project.is_distribution:  # pragma: no cover\n            raise ProjectError(\"tool.pdm.distribution must be `true` to be built.\")\n        if not wheel and not sdist:\n            project.core.ui.echo(\"All artifacts are disabled, nothing to do.\", err=True)\n            return\n        if not os.path.isabs(dest):\n            dest = project.root.joinpath(dest).as_posix()\n        if clean:\n            shutil.rmtree(dest, ignore_errors=True)\n        if not os.path.exists(dest):\n            os.makedirs(dest, exist_ok=True)\n        hooks.try_emit(\"pre_build\", dest=dest, config_settings=config_settings)\n        artifacts: list[str] = []\n        with project.core.ui.logging(\"build\"):\n            if not project.config[\"use_uv\"]:\n                if sdist:\n                    project.core.ui.echo(\"[info]Building sdist...\")\n                    sdist_file = SdistBuilder(project.root, project.environment).build(dest)\n                    project.core.ui.echo(f\"[info]Built sdist at {sdist_file}\")\n                    artifacts.append(sdist_file)\n                if wheel:\n                    if sdist:\n                        project.core.ui.echo(\"[info]Building wheel from sdist...\")\n                        sdist_out = tempfile.mkdtemp(prefix=\"pdm-build-via-sdist-\")\n                        try:\n                            with tarfile.open(sdist_file, \"r:gz\") as tf:\n                                tf.extractall(sdist_out)\n                                sdist_name = os.path.basename(sdist_file)[: -len(\".tar.gz\")]\n                                whl = WheelBuilder(os.path.join(sdist_out, sdist_name), project.environment).build(dest)\n                                project.core.ui.echo(f\"[info]Built wheel at {whl}\")\n                                artifacts.append(whl)\n                        finally:\n                            shutil.rmtree(sdist_out, ignore_errors=True)\n                    else:\n                        project.core.ui.echo(\"[info]Building wheel...\")\n                        whl = WheelBuilder(project.root, project.environment).build(dest)\n                        project.core.ui.echo(f\"[info]Built wheel at {whl}\")\n                        artifacts.append(whl)\n            else:\n                import subprocess\n\n                dest_dir = Path(dest).absolute()\n\n                uv_build_cmd = [*project.core.uv_cmd, \"build\", \"--out-dir\", str(dest_dir)]\n                if verbose == -1:\n                    uv_build_cmd.append(\"-q\")\n                elif verbose > 0:\n                    uv_build_cmd.append(f\"-{'v' * verbose}\")\n                subprocess.run(uv_build_cmd, check=True)\n\n                # pdm build doesn't include .gitignore, and pdm publish would fail with .gitignore\n                (dest_dir / \".gitignore\").unlink(missing_ok=True)\n                for sdist_fp in dest_dir.glob(\"*.tar.gz\"):\n                    if sdist is False:\n                        sdist_fp.unlink(missing_ok=True)\n                    else:\n                        artifacts.append(str(sdist_fp))\n\n                for whl_file in dest_dir.glob(\"*.whl\"):\n                    if wheel is False:\n                        whl_file.unlink(missing_ok=True)\n                    else:\n                        artifacts.append(str(whl_file))\n\n        hooks.try_emit(\"post_build\", artifacts=artifacts, config_settings=config_settings)\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\n            \"--no-sdist\",\n            dest=\"sdist\",\n            default=True,\n            action=\"store_false\",\n            help=\"Don't build source tarballs\",\n        )\n        parser.add_argument(\n            \"--no-wheel\",\n            dest=\"wheel\",\n            default=True,\n            action=\"store_false\",\n            help=\"Don't build wheels\",\n        )\n        parser.add_argument(\"-d\", \"--dest\", default=\"dist\", help=\"Target directory to put artifacts\")\n        parser.add_argument(\n            \"--no-clean\",\n            dest=\"clean\",\n            default=True,\n            action=\"store_false\",\n            help=\"Do not clean the target directory\",\n        )\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        self.do_build(\n            project,\n            sdist=options.sdist,\n            wheel=options.wheel,\n            dest=options.dest,\n            clean=options.clean,\n            verbose=options.verbose,\n            hooks=HookManager(project, options.skip),\n        )\n"
  },
  {
    "path": "src/pdm/cli/commands/cache.py",
    "content": "import argparse\nimport os\nfrom pathlib import Path\nfrom typing import Iterable\n\nfrom pdm import termui\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.options import verbose_option\nfrom pdm.exceptions import PdmUsageError\nfrom pdm.project import Project\n\n\nclass Command(BaseCommand):\n    \"\"\"Control the caches of PDM\"\"\"\n\n    arguments = (verbose_option,)\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        subparsers = parser.add_subparsers(title=\"commands\", metavar=\"\")\n        ClearCommand.register_to(subparsers, \"clear\")\n        RemoveCommand.register_to(subparsers, \"remove\")\n        ListCommand.register_to(subparsers, \"list\")\n        InfoCommand.register_to(subparsers, \"info\")\n        parser.set_defaults(search_parent=False)\n        self.parser = parser\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        self.parser.print_help()\n\n\ndef file_size(file: Path) -> int:\n    if file.is_symlink():\n        return 0\n    return os.path.getsize(file)\n\n\ndef find_files(parent: Path, pattern: str) -> Iterable[Path]:\n    for file in parent.rglob(pattern):\n        if file.is_file() or file.is_symlink():\n            yield file\n\n\ndef directory_size(directory: Path) -> int:\n    return sum(map(file_size, find_files(directory, \"*\")))\n\n\ndef format_size(size: float) -> str:\n    if size > 1000 * 1000:\n        return f\"{size / 1000.0 / 1000:.1f} MB\"\n    elif size > 10 * 1000:\n        return f\"{int(size / 1000)} kB\"\n    elif size > 1000:\n        return f\"{size / 1000.0:.1f} kB\"\n    else:\n        return f\"{int(size)} bytes\"\n\n\ndef remove_cache_files(project: Project, pattern: str) -> None:\n    if not pattern:\n        raise PdmUsageError(\"Please provide a pattern\")\n\n    wheel_cache = project.cache(\"wheels\")\n    files = list(find_files(wheel_cache, pattern))\n\n    if not files:\n        raise PdmUsageError(\"No matching files found\")\n\n    for file in files:\n        os.unlink(file)\n        project.core.ui.echo(f\"Removed {file}\", verbosity=termui.Verbosity.DETAIL)\n    project.core.ui.echo(f\"{len(files)} file{'s' if len(files) > 1 else ''} removed\")\n\n\nclass ClearCommand(BaseCommand):\n    \"\"\"Clean all the files under cache directory\"\"\"\n\n    arguments = (verbose_option,)\n    CACHE_TYPES = (\"hashes\", \"http\", \"wheels\", \"metadata\", \"packages\")\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\n            \"type\",\n            nargs=\"?\",\n            help=\"Clear the given type of caches\",\n            choices=self.CACHE_TYPES,\n        )\n\n    @staticmethod\n    def _clear_files(root: Path) -> int:\n        files = list(find_files(root, \"*\"))\n        for file in files:\n            os.unlink(file)\n        return len(files)\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        if not options.type:\n            types: Iterable[str] = self.CACHE_TYPES\n        else:\n            types = (str(options.type),)\n\n        packages = files = 0\n        with project.core.ui.open_spinner(f\"Clearing {options.type or 'all'} caches...\"):\n            for type_ in types:\n                if type_ == \"packages\":\n                    packages += project.package_cache.cleanup()\n                else:\n                    files += self._clear_files(project.cache(type_))\n            message = []\n            if packages:\n                message.append(f\"{packages} package{'s' if packages > 1 else ''}\")\n            if files:\n                message.append(f\"{files} file{'s' if files > 1 else ''}\")\n            if not message:  # pragma: no cover\n                text = \"No files need to be removed\"\n            else:\n                text = f\"{' and '.join(message)} are removed\"\n        project.core.ui.echo(text)\n\n\nclass RemoveCommand(BaseCommand):\n    \"\"\"Remove files matching the given pattern\"\"\"\n\n    arguments = (verbose_option,)\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\"pattern\", help=\"The pattern to remove\")\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        return remove_cache_files(project, options.pattern)\n\n\nclass ListCommand(BaseCommand):\n    \"\"\"List the built wheels stored in the cache\"\"\"\n\n    arguments = (verbose_option,)\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\"pattern\", nargs=\"?\", default=\"*\", help=\"The pattern to list\")\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        rows = [\n            (format_size(file_size(file)), file.name) for file in find_files(project.cache(\"wheels\"), options.pattern)\n        ]\n        project.core.ui.display_columns(rows, [\">Size\", \"Filename\"])\n\n\nclass InfoCommand(BaseCommand):\n    \"\"\"Show the info and current size of caches\"\"\"\n\n    arguments = (verbose_option,)\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        with project.core.ui.open_spinner(\"Calculating cache files\"):\n            output = [\n                f\"[primary]Cache Root[/]: {project.cache_dir}, \"\n                f\"Total size: {format_size(directory_size(project.cache_dir))}\"\n            ]\n            for name, description in [\n                (\"hashes\", \"File Hash Cache\"),\n                (\"http\", \"HTTP Cache\"),\n                (\"wheels\", \"Wheels Cache\"),\n                (\"metadata\", \"Metadata Cache\"),\n                (\"packages\", \"Package Cache\"),\n            ]:\n                cache_location = project.cache(name)\n                size = directory_size(cache_location)\n                output.append(f\"  [primary]{description}[/]: {cache_location}\")\n                if name == \"packages\":\n                    packages = list(project.package_cache.iter_packages())\n                    output.append(f\"    Packages: {len(packages)}, Size: {format_size(size)}\")\n                else:\n                    files = list(find_files(cache_location, \"*\"))\n                    output.append(f\"    Files: {len(files)}, Size: {format_size(size)}\")\n\n        project.core.ui.echo(\"\\n\".join(output))\n"
  },
  {
    "path": "src/pdm/cli/commands/completion.py",
    "content": "from __future__ import annotations\n\nimport argparse\nimport sys\n\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.compat import resources_read_text\nfrom pdm.exceptions import PdmUsageError\nfrom pdm.project import Project\n\n\nclass Command(BaseCommand):\n    \"\"\"Generate completion scripts for the given shell\"\"\"\n\n    arguments = ()\n    SUPPORTED_SHELLS = (\"bash\", \"zsh\", \"fish\", \"powershell\", \"pwsh\")\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\n            \"shell\",\n            nargs=\"?\",\n            help=\"The shell to generate the scripts for. If not given, PDM will properly guess from `SHELL` env var.\",\n        )\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        import shellingham\n\n        shell = options.shell or shellingham.detect_shell()[0]\n        if shell not in self.SUPPORTED_SHELLS:\n            raise PdmUsageError(f\"Unsupported shell: {shell}\")\n        suffix = \"ps1\" if shell in {\"powershell\", \"pwsh\"} else shell\n        completion = resources_read_text(\"pdm.cli.completions\", f\"pdm.{suffix}\")\n        # Can't use rich print or otherwise the rich markups will be interpreted\n        print(completion.replace(\"%{python_executable}\", sys.executable))\n"
  },
  {
    "path": "src/pdm/cli/commands/config.py",
    "content": "import argparse\nimport os\nfrom pathlib import Path\nfrom typing import Any, Mapping\n\nfrom pdm import termui\nfrom pdm._types import RepositoryConfig\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.exceptions import PdmUsageError\nfrom pdm.project import Project\nfrom pdm.project.config import DEFAULT_REPOSITORIES, REPOSITORY, SOURCE, Config\n\n\nclass Command(BaseCommand):\n    \"\"\"Display the current configuration\"\"\"\n\n    ui: termui.UI\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\n            \"-l\",\n            \"--local\",\n            action=\"store_true\",\n            help=\"Set config in the project's local configuration file\",\n        )\n        parser.add_argument(\"-d\", \"--delete\", action=\"store_true\", help=\"Unset a configuration key\")\n        parser.add_argument(\n            \"-e\",\n            \"--edit\",\n            action=\"store_true\",\n            help=\"Edit the configuration file in the default editor(defined by EDITOR env var)\",\n        )\n        parser.add_argument(\"key\", help=\"Config key\", nargs=\"?\")\n        parser.add_argument(\"value\", help=\"Config value\", nargs=\"?\")\n\n    @staticmethod\n    def get_editor() -> str:\n        for key in \"VISUAL\", \"EDITOR\":\n            rv = os.getenv(key)\n            if rv:\n                return rv\n        if os.name == \"nt\":\n            return \"notepad\"\n        for editor in \"sensible-editor\", \"vim\", \"nano\":\n            if os.system(f\"which {editor} >/dev/null 2>&1\") == 0:\n                return editor\n        return \"vi\"\n\n    def edit_file(self, path: Path) -> None:\n        import subprocess\n\n        editor = self.get_editor()\n        proc = subprocess.Popen(f'{editor} \"{path}\"', shell=True)\n\n        if proc.wait() != 0:\n            raise PdmUsageError(f\"Editor {editor} exited abnormally\")\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        self.ui = project.core.ui\n        if options.edit:\n            if options.key:\n                raise PdmUsageError(\"Cannot specify an argument when `--edit` is given\")\n            if options.delete:\n                raise PdmUsageError(\"`--delete` doesn't work when `--edit` is given\")\n            config = project.project_config if options.local else project.global_config\n            config_path = config.config_file\n            config_path.parent.mkdir(parents=True, exist_ok=True)\n            return self.edit_file(config_path)\n        if options.delete:\n            self._delete_config(project, options)\n        elif options.value:\n            self._set_config(project, options)\n        elif options.key:\n            self._get_config(project, options)\n        else:\n            self._list_config(project, options)\n\n    def _get_config(self, project: Project, options: argparse.Namespace) -> None:\n        from findpython import ALL_PROVIDERS\n\n        if options.key in project.project_config.deprecated:  # pragma: no cover\n            project.core.ui.warn(\n                f\"[warning]DEPRECATED:[/] the config has been renamed to {project.project_config.deprecated[options.key]}\",\n            )\n            options.key = project.project_config.deprecated[options.key]\n        try:\n            value = project.project_config[options.key]\n        except KeyError:\n            value = project.global_config[options.key]\n        if options.key.endswith(\".password\"):\n            value = \"[i]<hidden>[/i]\"\n        elif options.key == \"python.providers\" and not value:\n            value = [\"venv\", *ALL_PROVIDERS]\n        project.core.ui.echo(value)\n\n    def _set_config(self, project: Project, options: argparse.Namespace) -> None:\n        config = project.project_config if options.local else project.global_config\n        if options.key in config.deprecated:  # pragma: no cover\n            project.core.ui.warn(\n                f\"[warning]DEPRECATED:[/] the config has been renamed to {config.deprecated[options.key]}\",\n            )\n        config[options.key] = options.value\n\n    def _show_config(self, config: Mapping[str, Any], supersedes: Mapping[str, Any]) -> None:\n        from findpython import ALL_PROVIDERS\n\n        assert Config.site is not None\n        for key in sorted(config):\n            deprecated = \"\"\n            canonical_key = key\n            superseded = key in supersedes\n            if key in Config.site.deprecated:  # pragma: no cover\n                canonical_key = Config.site.deprecated[key]\n                if canonical_key in supersedes:\n                    superseded = True\n                deprecated = f\"[error](deprecating: {key})[/]\"\n            elif key not in Config._config_map and not (key.startswith(\"pypi.\") or key.startswith(REPOSITORY)):\n                continue\n            extra_style = \"dim\" if superseded else None\n            if canonical_key not in Config._config_map:\n                prefix, name = key.split(\".\", 1)\n                if prefix in (SOURCE, REPOSITORY):\n                    title = \"non-default PyPI index\" if prefix == SOURCE else \"custom repository\"\n                    self.ui.echo(\n                        f\"[warning]# Configuration of {title} `{name}`\",\n                        style=extra_style,\n                        verbosity=termui.Verbosity.DETAIL,\n                    )\n                    repository = RepositoryConfig(**config[key], config_prefix=prefix, name=name)\n                    if not repository.url and name in DEFAULT_REPOSITORIES:\n                        repository.url = DEFAULT_REPOSITORIES[name]\n                    self.ui.echo(repository)\n                continue\n            config_item = Config._config_map[canonical_key]\n            self.ui.echo(\n                f\"[warning]# {config_item.description}\",\n                style=extra_style,\n                verbosity=termui.Verbosity.DETAIL,\n            )\n            if key.endswith(\"password\"):\n                value: Any = \"[i]<hidden>[/i]\"\n            else:\n                value = config[key]\n                if key == \"python.providers\" and not value:\n                    value = [\"venv\", *ALL_PROVIDERS]\n            self.ui.echo(\n                f\"[primary]{canonical_key}[/]{deprecated} = {value}\",\n                style=extra_style,\n            )\n\n    def _list_config(self, project: Project, options: argparse.Namespace) -> None:\n        assert Config.site is not None\n        site_title = \"Site/default configuration\"\n        if Config.site.config_file.exists():\n            site_title += f\" ([success]{Config.site.config_file}[/])\"\n        self.ui.echo(site_title, style=\"bold\")\n        self._show_config(\n            Config.get_defaults(),\n            {**project.global_config.self_data, **project.project_config.self_data},\n        )\n\n        if project.global_config.self_data:\n            self.ui.echo(\n                f\"\\nHome configuration ([success]{project.global_config.config_file}[/]):\",\n                style=\"bold\",\n            )\n            self._show_config(project.global_config.self_data, project.project_config.self_data)\n\n        if project.project_config.self_data:\n            self.ui.echo(\n                f\"\\nProject configuration ([success]{project.project_config.config_file}[/]):\",\n                style=\"bold\",\n            )\n            self._show_config(project.project_config.self_data, {})\n\n    def _delete_config(self, project: Project, options: argparse.Namespace) -> None:\n        config = project.project_config if options.local else project.global_config\n        if options.key in config.deprecated:  # pragma: no cover\n            project.core.ui.warn(\n                f\"[warning]DEPRECATED:[/] the config has been renamed to {config.deprecated[options.key]}\",\n            )\n        del config[options.key]\n"
  },
  {
    "path": "src/pdm/cli/commands/export.py",
    "content": "from __future__ import annotations\n\nimport argparse\nfrom pathlib import Path\nfrom typing import Iterable\n\nimport tomlkit\n\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.filters import GroupSelection\nfrom pdm.cli.options import groups_group, lockfile_option\nfrom pdm.exceptions import PdmUsageError\nfrom pdm.formats import FORMATS\nfrom pdm.formats.pylock import PyLockConverter\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.requirements import Requirement\nfrom pdm.project import Project\nfrom pdm.project.lockfile import FLAG_INHERIT_METADATA\n\n\nclass Command(BaseCommand):\n    \"\"\"Export the locked packages set to other formats\"\"\"\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        lockfile_option.add_to_parser(parser)\n        parser.add_argument(\n            \"-f\",\n            \"--format\",\n            choices=[\"requirements\", \"pylock\"],\n            default=\"requirements\",\n            help=\"Export to requirements.txt format or pylock.toml format\",\n        )\n        groups_group.add_to_parser(parser)\n        parser.add_argument(\n            \"--no-hashes\",\n            \"--without-hashes\",\n            dest=\"hashes\",\n            action=\"store_false\",\n            default=True,\n            help=\"Don't include artifact hashes\",\n        )\n        parser.add_argument(\n            \"--no-markers\",\n            action=\"store_false\",\n            default=True,\n            dest=\"markers\",\n            help=\"(DEPRECATED)Don't include platform markers\",\n        )\n        parser.add_argument(\n            \"--no-extras\", action=\"store_false\", default=True, dest=\"extras\", help=\"Strip extras from the requirements\"\n        )\n        parser.add_argument(\n            \"-o\",\n            \"--output\",\n            help=\"Write output to the given file, or print to stdout if not given\",\n        )\n        parser.add_argument(\n            \"--pyproject\",\n            action=\"store_true\",\n            help=\"Read the list of packages from pyproject.toml\",\n        )\n        parser.add_argument(\"--expandvars\", action=\"store_true\", help=\"Expand environment variables in requirements\")\n        group = parser.add_mutually_exclusive_group()\n        group.add_argument(\"--self\", action=\"store_true\", help=\"Include the project itself\")\n        group.add_argument(\n            \"--editable-self\", action=\"store_true\", help=\"Include the project itself as an editable dependency\"\n        )\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        from pdm.models.repositories.lock import Package\n\n        if options.format == \"pylock\":\n            locked_repository = project.get_locked_repository()\n            if options.self or options.editable_self:\n                locked_repository.add_package(\n                    Package(project.make_self_candidate(editable=options.editable_self), [], \"\")\n                )\n            doc = tomlkit.dumps(PyLockConverter(project, locked_repository).convert())\n            if options.output:\n                Path(options.output).write_text(doc, encoding=\"utf-8\")\n            else:\n                print(doc)\n            return\n        if options.pyproject:\n            options.hashes = False\n        selection = GroupSelection.from_options(project, options)\n        if options.markers is False:\n            project.core.ui.warn(\n                \"The --no-markers option is on, the exported requirements can only work on the current platform\"\n            )\n        packages: Iterable[Requirement] | Iterable[Candidate]\n        if options.pyproject:\n            all_deps = project._resolve_dependencies(list(selection))\n            packages = [r for group in selection for r in project.get_dependencies(group, all_deps)]\n        else:\n            if not project.lockfile.exists():\n                raise PdmUsageError(\"No lockfile found, please run `pdm lock` first.\")\n            if FLAG_INHERIT_METADATA not in project.lockfile.strategy:\n                raise PdmUsageError(\n                    \"Can't export a lock file without environment markers, please re-generate the lock file with `inherit_metadata` strategy.\"\n                )\n            groups = set(selection)\n            candidates = sorted(\n                (\n                    entry.candidate\n                    for entry in project.get_locked_repository().evaluate_candidates(groups, not options.markers)\n                ),\n                key=lambda c: not c.req.extras,\n            )\n            packages = []\n            seen_extras: set[str] = set()\n            for candidate in candidates:\n                if options.extras:\n                    key = candidate.req.key or \"\"\n                    if candidate.req.extras:\n                        seen_extras.add(key)\n                    elif key in seen_extras:\n                        continue\n                elif candidate.req.extras:\n                    continue\n                if not options.markers and candidate.req.marker:\n                    candidate.req.marker = None\n                packages.append(candidate)  # type: ignore[arg-type]\n\n        content = FORMATS[options.format].export(project, packages, options)\n        if options.output:\n            Path(options.output).write_text(content, encoding=\"utf-8\")\n        else:\n            # Use a regular print to avoid any formatting / wrapping.\n            print(content)\n"
  },
  {
    "path": "src/pdm/cli/commands/fix/__init__.py",
    "content": "from __future__ import annotations\n\nimport argparse\n\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.commands.fix.fixers import BaseFixer, LockStrategyFixer, PackageTypeFixer, ProjectConfigFixer\nfrom pdm.exceptions import PdmUsageError\nfrom pdm.project import Project\nfrom pdm.termui import Emoji\n\n\nclass Command(BaseCommand):\n    \"\"\"Fix the project problems according to the latest version of PDM\"\"\"\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\"problem\", nargs=\"?\", help=\"Fix the specific problem, or all if not given\")\n        parser.add_argument(\"--dry-run\", action=\"store_true\", help=\"Only show the problems\")\n\n    @staticmethod\n    def find_problems(project: Project) -> list[tuple[str, BaseFixer]]:\n        \"\"\"Get the problems in the project\"\"\"\n        problems: list[tuple[str, BaseFixer]] = []\n        for fixer in Command.get_fixers(project):\n            if fixer.check():\n                problems.append((fixer.identifier, fixer))\n        return problems\n\n    @staticmethod\n    def check_problems(project: Project, strict: bool = True) -> None:\n        \"\"\"Check the problems in the project\"\"\"\n        problems = Command.find_problems(project)\n        if not problems:\n            return\n        breaking = False\n        project.core.ui.warn(\"The following problems are found in your project:\")\n        for name, fixer in problems:\n            project.core.ui.echo(f\"  [b]{name}[/]: {fixer.get_message()}\", err=True)\n            if fixer.breaking:\n                breaking = True\n        extra_option = \" -g\" if project.is_global else \"\"\n        project.core.ui.echo(\n            f\"Run [success]pdm fix{extra_option}[/] to fix all or [success]pdm fix{extra_option} <name>[/]\"\n            \" to fix individual problem.\",\n            err=True,\n        )\n        if breaking and strict:\n            raise SystemExit(1)\n\n    @staticmethod\n    def get_fixers(project: Project) -> list[BaseFixer]:\n        \"\"\"Return a list of fixers to check, the order matters\"\"\"\n        return [ProjectConfigFixer(project), PackageTypeFixer(project), LockStrategyFixer(project)]\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        if options.dry_run:\n            return self.check_problems(project)\n        problems = self.find_problems(project)\n        if options.problem:\n            fixer = next((fixer for name, fixer in problems if name == options.problem), None)\n            if not fixer:\n                raise PdmUsageError(\n                    f\"The problem doesn't exist: [success]{options.problem}[/], \"\n                    f\"possible values are {[p[0] for p in problems]}\",\n                )\n            project.core.ui.echo(f\"Fixing [success]{fixer.identifier}[/]...\", end=\" \")\n            fixer.fix()\n            project.core.ui.echo(f\"[success]{Emoji.SUCC}[/]\")\n            return\n        if not problems:\n            project.core.ui.echo(\"No problem is found, nothing to fix.\")\n            return\n        for name, fixer in problems:\n            project.core.ui.echo(f\"Fixing [success]{name}[/]...\", end=\" \")\n            fixer.fix()\n            project.core.ui.echo(f\"[success]{Emoji.SUCC}[/]\")\n"
  },
  {
    "path": "src/pdm/cli/commands/fix/fixers.py",
    "content": "import abc\nimport re\n\nfrom pdm.project import Config, Project\nfrom pdm.project.lockfile import FLAG_CROSS_PLATFORM\nfrom pdm.termui import Verbosity\nfrom pdm.utils import parse_version\n\n\nclass BaseFixer(abc.ABC):\n    \"\"\"Base class for fixers\"\"\"\n\n    # A unique identifier for the fixer\n    identifier: str\n    # A boolean flag to indicate if the problem is breaking\n    breaking: bool = False\n\n    def __init__(self, project: Project) -> None:\n        self.project = project\n\n    def log(self, message: str, verbosity: Verbosity = Verbosity.DETAIL) -> None:\n        self.project.core.ui.echo(message, verbosity=verbosity)\n\n    @abc.abstractmethod\n    def get_message(self) -> str:\n        \"\"\"Return a description of the problem\"\"\"\n\n    @abc.abstractmethod\n    def fix(self) -> None:\n        \"\"\"Perform the fix\"\"\"\n\n    @abc.abstractmethod\n    def check(self) -> bool:\n        \"\"\"Check if the problem exists\"\"\"\n\n\nclass ProjectConfigFixer(BaseFixer):\n    \"\"\"Fix the project config\"\"\"\n\n    identifier = \"project-config\"\n\n    def get_message(self) -> str:\n        return (\n            \"[success]python.path[/] config needs to be moved to [info].pdm-python[/] and \"\n            \"[info].pdm.toml[/] needs to be renamed to [info]pdm.toml[/]\"\n        )\n\n    def _fix_gitignore(self) -> None:\n        gitignore = self.project.root.joinpath(\".gitignore\")\n        if not gitignore.exists():\n            return\n        content = gitignore.read_text(\"utf8\")\n        if \".pdm-python\" not in content:\n            content = re.sub(r\"^\\.pdm\\.toml$\", \".pdm-python\", content, flags=re.M)\n            gitignore.write_text(content, \"utf8\")\n\n    def fix(self) -> None:\n        old_file = self.project.root.joinpath(\".pdm.toml\")\n        config = Config(old_file).self_data\n        if not self.project.root.joinpath(\".pdm-python\").exists() and config.get(\"python.path\"):\n            self.log(\"Creating .pdm-python...\", verbosity=Verbosity.DETAIL)\n            self.project.root.joinpath(\".pdm-python\").write_text(config[\"python.path\"])\n        self.project.project_config  # access the project config to move the config items\n        self.log(\"Moving .pdm.toml to pdm.toml...\", verbosity=Verbosity.DETAIL)\n        old_file.unlink()\n        self.log(\"Fixing .gitignore...\", verbosity=Verbosity.DETAIL)\n        self._fix_gitignore()\n\n    def check(self) -> bool:\n        return self.project.root.joinpath(\".pdm.toml\").exists()\n\n\nclass PackageTypeFixer(BaseFixer):  # pragma: no cover\n    identifier = \"package-type\"\n\n    def get_message(self) -> str:\n        package_type = self.project.pyproject.settings[\"package-type\"]\n        dist = str(package_type == \"library\").lower()\n        return (\n            rf'[success]package-type = \"{package_type}\"[/] has been renamed to '\n            rf\"[info]distribution = {dist}[/] under \\[tool.pdm] table\"\n        )\n\n    def check(self) -> bool:\n        return \"package-type\" in self.project.pyproject.settings\n\n    def fix(self) -> None:\n        # Copy the project settings\n        self.project.pyproject.open_for_write()\n        settings = self.project.pyproject.settings.copy()\n\n        # Pop the package type and convert it to a distribution type\n        package_type = settings.pop(\"package-type\")\n        dist = package_type == \"library\"\n        settings[\"distribution\"] = dist\n\n        # Update the project settings with the new distribution type\n        self.project.pyproject._data[\"tool\"].pop(\"pdm\")\n        self.project.pyproject.settings.update(settings)\n\n        # Write the updated settings back to the project\n        self.project.pyproject.write(False)\n\n\nclass LockStrategyFixer(BaseFixer):\n    identifier = \"deprecated-cross-platform\"\n\n    def get_message(self) -> str:\n        return \"Lock strategy [success]`cross_platform`[/] has been deprecated in favor of lock targets.\"\n\n    def check(self) -> bool:\n        from pdm.project.lockfile import PDMLock\n\n        lockfile = self.project.lockfile\n        if not isinstance(lockfile, PDMLock):  # pragma: no cover\n            return False\n        lockfile_version = lockfile.file_version\n        if not lockfile_version or parse_version(lockfile_version) < parse_version(\"4.5.0\"):\n            return False\n        return FLAG_CROSS_PLATFORM in lockfile.strategy\n\n    def fix(self) -> None:\n        strategies = self.project.lockfile.strategy - {FLAG_CROSS_PLATFORM}\n        lockfile = self.project.lockfile.open_for_write()\n        lockfile[\"metadata\"][\"strategy\"] = sorted(strategies)\n        self.project.lockfile.write(False)\n        self.log(\"Lock strategy [success]`cross_platform` has been removed.\", verbosity=Verbosity.DETAIL)\n"
  },
  {
    "path": "src/pdm/cli/commands/import_cmd.py",
    "content": "from __future__ import annotations\n\nimport argparse\n\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.exceptions import PdmUsageError\nfrom pdm.formats import FORMATS\nfrom pdm.project import Project\n\n\nclass Command(BaseCommand):\n    \"\"\"Import project metadata from other formats\"\"\"\n\n    name = \"import\"\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\n            \"-d\",\n            \"--dev\",\n            default=False,\n            action=\"store_true\",\n            help=\"import packages into dev dependencies\",\n        )\n        parser.add_argument(\"-G\", \"--group\", help=\"Specify the target dependency group to import into\")\n        parser.add_argument(\n            \"-f\",\n            \"--format\",\n            choices=FORMATS.keys(),\n            help=\"Specify the file format explicitly\",\n        )\n        parser.add_argument(\"filename\", help=\"The file name\")\n        parser.set_defaults(search_parent=False)\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        self.do_import(project, options.filename, options.format, options)\n\n    @staticmethod\n    def do_import(\n        project: Project,\n        filename: str,\n        format: str | None = None,\n        options: argparse.Namespace | None = None,\n        reset_backend: bool = True,\n    ) -> None:\n        \"\"\"Import project metadata from given file.\n\n        :param project: the project instance\n        :param filename: the file name\n        :param format: the file format, or guess if not given.\n        :param options: other options parsed to the CLI.\n        \"\"\"\n        import tomlkit\n\n        from pdm.cli.utils import merge_dictionary\n        from pdm.formats import FORMATS\n        from pdm.models.backends import DEFAULT_BACKEND\n\n        if not format:\n            for key in FORMATS:\n                if FORMATS[key].check_fingerprint(project, filename):\n                    break\n            else:\n                raise PdmUsageError(\n                    \"Can't derive the file format automatically, please specify it via '-f/--format' option.\"\n                )\n        else:\n            key = format\n        if options is None:\n            options = argparse.Namespace(dev=False, group=None)\n        project_data, settings = FORMATS[key].convert(project, filename, options)\n        dependency_groups = settings.pop(\"dev-dependencies\", {})  # type: ignore[attr-defined]\n        pyproject = project.pyproject.open_for_write()\n\n        if \"tool\" not in pyproject or \"pdm\" not in pyproject[\"tool\"]:\n            pyproject.setdefault(\"tool\", {})[\"pdm\"] = tomlkit.table()\n        if \"build\" in pyproject[\"tool\"][\"pdm\"] and isinstance(pyproject[\"tool\"][\"pdm\"][\"build\"], str):\n            pyproject[\"tool\"][\"pdm\"][\"build\"] = {\n                \"setup-script\": pyproject[\"tool\"][\"pdm\"][\"build\"],\n                \"run-setuptools\": True,\n            }\n        if \"project\" not in pyproject:\n            pyproject.add(\"project\", tomlkit.table())\n            pyproject[\"project\"].add(tomlkit.comment(\"PEP 621 project metadata\"))\n            pyproject[\"project\"].add(tomlkit.comment(\"See https://www.python.org/dev/peps/pep-0621/\"))\n\n        merge_dictionary(pyproject[\"project\"], project_data)\n        dynamic_fields = pyproject[\"project\"].get(\"dynamic\", [])\n        if \"dependencies\" in project_data and \"dependencies\" in dynamic_fields:\n            dynamic_fields.remove(\"dependencies\")\n        if \"optional-dependencies\" in project_data and \"optional-dependencies\" in dynamic_fields:\n            dynamic_fields.remove(\"optional-dependencies\")\n        merge_dictionary(pyproject[\"tool\"][\"pdm\"], settings)\n        if dependency_groups:\n            merge_dictionary(pyproject.setdefault(\"dependency-groups\", {}), dependency_groups)\n        if reset_backend:\n            pyproject[\"build-system\"] = DEFAULT_BACKEND.build_system()\n\n        if \"requires-python\" not in pyproject[\"project\"]:\n            python_version = f\"{project.python.major}.{project.python.minor}\"\n            pyproject[\"project\"][\"requires-python\"] = f\">={python_version}\"\n            project.core.ui.echo(\n                \"The project's [primary]requires-python[/] has been set to [primary]>=\"\n                f\"{python_version}[/]. You can change it later if necessary.\"\n            )\n        project.pyproject.write()\n"
  },
  {
    "path": "src/pdm/cli/commands/info.py",
    "content": "import argparse\nimport json\n\nfrom rich import print_json\n\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.options import ArgumentGroup, venv_option\nfrom pdm.cli.utils import check_project_file\nfrom pdm.project import Project\n\n\nclass Command(BaseCommand):\n    \"\"\"Show the project information\"\"\"\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        venv_option.add_to_parser(parser)\n        group = ArgumentGroup(\"fields\", is_mutually_exclusive=True)\n        group.add_argument(\"--python\", action=\"store_true\", help=\"Show the interpreter path\")\n        group.add_argument(\n            \"--where\",\n            dest=\"where\",\n            action=\"store_true\",\n            help=\"Show the project root path\",\n        )\n        group.add_argument(\"--packages\", action=\"store_true\", help=\"Show the local packages root\")\n        group.add_argument(\"--env\", action=\"store_true\", help=\"Show PEP 508 environment markers\")\n        group.add_argument(\"--json\", action=\"store_true\", help=\"Dump the information in JSON\")\n        group.add_to_parser(parser)\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        check_project_file(project)\n        interpreter = project.environment.interpreter\n        packages_path = \"\"\n        if project.environment.is_local:\n            packages_path = project.environment.packages_path  # type: ignore[attr-defined]\n        else:\n            # For virtual environments and other non-local environments,\n            # show the site-packages path (purelib is the standard location)\n            paths = project.environment.get_paths()\n            packages_path = paths.get(\"purelib\", \"\")\n        if options.python:\n            project.core.ui.echo(str(interpreter.executable))\n        elif options.where:\n            project.core.ui.echo(str(project.root))\n        elif options.packages:\n            project.core.ui.echo(str(packages_path))\n        elif options.env:\n            project.core.ui.echo(json.dumps(project.environment.spec.markers_with_defaults(), indent=2))\n        elif options.json:\n            print_json(\n                data={\n                    \"pdm\": {\"version\": project.core.version},\n                    \"python\": {\n                        \"interpreter\": str(interpreter.executable),\n                        \"version\": interpreter.identifier,\n                        \"markers\": project.environment.spec.markers_with_defaults(),\n                    },\n                    \"project\": {\n                        \"root\": str(project.root),\n                        \"pypackages\": str(packages_path),\n                    },\n                }\n            )\n        else:\n            for name, value in zip(\n                [\n                    f\"[primary]{key}[/]:\"\n                    for key in [\n                        \"PDM version\",\n                        f\"{'Global ' if project.is_global else ''}Python Interpreter\",\n                        f\"{'Global ' if project.is_global else ''}Project Root\",\n                        f\"{'Global ' if project.is_global else ''}Local Packages\",\n                    ]\n                ],\n                [\n                    project.core.version,\n                    f\"{interpreter.executable} ({interpreter.identifier})\",\n                    project.root.as_posix(),\n                    str(packages_path),\n                ],\n            ):\n                project.core.ui.echo(f\"{name}\\n  {value}\")\n"
  },
  {
    "path": "src/pdm/cli/commands/init.py",
    "content": "from __future__ import annotations\n\nimport argparse\nfrom typing import TYPE_CHECKING, Any, cast\n\nfrom pdm import termui\nfrom pdm.cli import actions\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.hooks import HookManager\nfrom pdm.cli.options import skip_option\nfrom pdm.cli.templates import ProjectTemplate\nfrom pdm.exceptions import PdmUsageError, ProjectError\nfrom pdm.models.backends import _BACKENDS, DEFAULT_BACKEND, BuildBackend, get_backend\nfrom pdm.models.specifiers import get_specifier\nfrom pdm.utils import (\n    get_user_email_from_git,\n    package_installed,\n    sanitize_project_name,\n    validate_project_name,\n)\n\nif TYPE_CHECKING:\n    from pdm.project import Project\n\n\nclass Command(BaseCommand):\n    \"\"\"Initialize a pyproject.toml for PDM.\n\n    Built-in templates:\n    - default: `pdm init`, A simple template with a basic structure.\n    - minimal: `pdm init minimal`, A minimal template with only `pyproject.toml`.\n    \"\"\"\n\n    supports_other_generator = True\n\n    def __init__(self) -> None:\n        self.interactive = True\n\n    def initialize_git(self, project: Project) -> None:\n        \"\"\"Initialize a git repository if git is available and .git doesn't exist.\"\"\"\n        import shutil\n        import subprocess\n\n        if (project.root / \".git\").exists():\n            project.core.ui.info(\"Git repository already exists, skipping initialization.\")\n            return\n\n        git_command = shutil.which(\"git\")\n        if not git_command:\n            project.core.ui.info(\"Git command not found, skipping initialization.\")\n            return\n\n        try:\n            subprocess.run(\n                [git_command, \"init\"],\n                cwd=project.root,\n                check=True,\n                capture_output=True,\n                encoding=\"utf-8\",\n            )\n            project.core.ui.info(\"Git repository initialized successfully.\")\n        except subprocess.CalledProcessError as e:\n            project.core.ui.error(f\"Failed to initialize Git repository: {e.stderr}\")\n\n    def do_init(self, project: Project, options: argparse.Namespace) -> None:\n        \"\"\"Bootstrap the project and create a pyproject.toml\"\"\"\n        hooks = HookManager(project, options.skip)\n        if options.generator == \"copier\":\n            self._init_copier(project, options)\n        elif options.generator == \"cookiecutter\":\n            self._init_cookiecutter(project, options)\n        else:\n            self.set_python(project, options.python, hooks)\n            self._init_builtin(project, options)\n\n        if options.init_git:\n            self.initialize_git(project)\n\n        hooks.try_emit(\"post_init\")\n\n    def _init_copier(self, project: Project, options: argparse.Namespace) -> None:\n        if not package_installed(\"copier\"):\n            raise PdmUsageError(\n                \"--copier is passed but copier is not installed. Install it by `pdm self add copier`\"\n            ) from None\n\n        from copier.cli import CopierApp\n\n        if not options.template:\n            raise PdmUsageError(\"template argument is required when --copier is passed\")\n        _, retval = CopierApp.run(\n            [\"copier\", \"copy\", options.template, str(project.root), *options.generator_args], exit=False\n        )\n        if retval != 0:\n            raise RuntimeError(\"Copier exited with non-zero status code\")\n\n    def _init_cookiecutter(self, project: Project, options: argparse.Namespace) -> None:\n        if not package_installed(\"cookiecutter\"):\n            raise PdmUsageError(\n                \"--cookiecutter is passed but cookiecutter is not installed. Install it by `pdm self add cookiecutter`\"\n            ) from None\n\n        from cookiecutter.cli import main as cookiecutter\n\n        if not options.template:\n            raise PdmUsageError(\"template argument is required when --cookiecutter is passed\")\n        try:\n            cookiecutter.main(\n                [options.template, \"--output-dir\", str(project.root), *options.generator_args], standalone_mode=False\n            )\n        except SystemExit as e:\n            raise RuntimeError(\"Cookiecutter exited with an error\") from e\n\n    def _init_builtin(self, project: Project, options: argparse.Namespace) -> None:\n        metadata = self.get_metadata_from_input(project, options)\n        template = options.template\n        if not template:\n            template = \"default\" if options.dist else \"minimal\"\n        with ProjectTemplate(template) as template:\n            template.generate(project.root, metadata, options.overwrite)\n        project.pyproject.reload()\n\n    def set_interactive(self, value: bool) -> None:\n        self.interactive = value\n\n    def ask(self, question: str, default: str) -> str:\n        if not self.interactive:\n            return default\n        return termui.ask(question, default=default)\n\n    def ask_project(self, project: Project) -> str:\n        default = sanitize_project_name(project.root.name)\n        name = self.ask(\"Project name\", default)\n        if default == name or validate_project_name(name):\n            return name\n        project.core.ui.echo(\n            \"Project name is not valid, it should follow PEP 426\",\n            err=True,\n            style=\"warning\",\n        )\n        return self.ask_project(project)\n\n    def get_metadata_from_input(self, project: Project, options: argparse.Namespace) -> dict[str, Any]:\n        from pdm.formats.base import array_of_inline_tables, make_array, make_inline_table\n\n        if options.name:\n            if not validate_project_name(options.name):\n                raise ProjectError(\"Project name is not valid, it should follow PEP 426\")\n            name = options.name\n        else:\n            name = self.ask_project(project)\n        version = self.ask(\"Project version\", options.project_version or \"0.1.0\")\n        is_dist = options.dist or bool(options.backend)\n        if not is_dist and self.interactive:\n            is_dist = termui.confirm(\n                \"Do you want to build this project for distribution(such as wheel)?\\n\"\n                \"If yes, it will be installed by default when running `pdm install`.\"\n            )\n        options.dist = is_dist\n        build_backend: type[BuildBackend] | None = None\n        python = project.python\n        if is_dist:\n            description = self.ask(\"Project description\", \"\")\n            if options.backend:\n                build_backend = get_backend(options.backend)\n            elif self.interactive:\n                all_backends = list(_BACKENDS)\n                project.core.ui.echo(\"Which build backend to use?\")\n                for i, backend in enumerate(all_backends):\n                    project.core.ui.echo(f\"{i}. [success]{backend}[/]\")\n                selected_backend = termui.ask(\n                    \"Please select\",\n                    prompt_type=int,\n                    choices=[str(i) for i in range(len(all_backends))],\n                    show_choices=False,\n                    default=0,\n                )\n                build_backend = get_backend(all_backends[int(selected_backend)])\n            else:\n                build_backend = DEFAULT_BACKEND\n            default_python_requires = f\">={python.major}.{python.minor}\"\n        else:\n            description = \"\"\n            default_python_requires = f\"=={python.major}.{python.minor}.*\"\n        license = self.ask(\"License(SPDX name)\", options.license or \"MIT\")\n\n        git_user, git_email = get_user_email_from_git()\n        author = self.ask(\"Author name\", git_user)\n        email = self.ask(\"Author email\", git_email)\n        python_requires = self.ask(\"Python requires('*' to allow any)\", default_python_requires)\n\n        data = {\n            \"project\": {\n                \"name\": name,\n                \"version\": version,\n                \"authors\": array_of_inline_tables([{\"name\": author, \"email\": email}]),\n                \"license\": make_inline_table({\"text\": license}),\n                \"dependencies\": make_array([], True),\n            },\n            \"tool\": {\"pdm\": {\"distribution\": is_dist}},\n        }\n\n        if python_requires and python_requires != \"*\":\n            get_specifier(python_requires)\n            data[\"project\"][\"requires-python\"] = python_requires  # type: ignore[index]\n        if description:\n            data[\"project\"][\"description\"] = description  # type: ignore[index]\n        if build_backend is not None:\n            data[\"build-system\"] = cast(dict, build_backend.build_system())\n\n        return data\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        skip_option.add_to_parser(parser)\n\n        status = {\n            False: termui.style(\"\\\\[not installed]\", style=\"error\"),\n            True: termui.style(\"\\\\[installed]\", style=\"success\"),\n        }\n        if self.supports_other_generator:\n            generator = parser.add_mutually_exclusive_group()\n            generator.add_argument(\n                \"--copier\",\n                action=\"store_const\",\n                dest=\"generator\",\n                const=\"copier\",\n                help=f\"Use Copier to generate project {status[package_installed('copier')]}\",\n            )\n            generator.add_argument(\n                \"--cookiecutter\",\n                action=\"store_const\",\n                dest=\"generator\",\n                const=\"cookiecutter\",\n                help=f\"Use Cookiecutter to generate project {status[package_installed('cookiecutter')]}\",\n            )\n        group = parser.add_argument_group(\"builtin generator options\")\n        group.add_argument(\n            \"-n\",\n            \"--non-interactive\",\n            action=\"store_true\",\n            help=\"Don't ask questions but use default values\",\n        )\n        group.add_argument(\"--python\", help=\"Specify the Python version/path to use\")\n        group.add_argument(\n            \"--dist\", \"--lib\", dest=\"dist\", action=\"store_true\", help=\"Create a package for distribution\"\n        )\n        group.add_argument(\"--backend\", choices=list(_BACKENDS), help=\"Specify the build backend, which implies --dist\")\n        group.add_argument(\"--license\", help=\"Specify the license (SPDX name)\")\n        group.add_argument(\"--name\", help=\"Specify the project name\")\n        group.add_argument(\"--project-version\", help=\"Specify the project's version\")\n        group.add_argument(\n            \"--no-git\", dest=\"init_git\", action=\"store_false\", default=True, help=\"Do not initialize a git repository\"\n        )\n        parser.add_argument(\n            \"template\", nargs=\"?\", help=\"Specify the project template, which can be a local path or a Git URL\"\n        )\n        if self.supports_other_generator:\n            parser.add_argument(\"generator_args\", nargs=argparse.REMAINDER, help=\"Arguments passed to the generator\")\n        parser.add_argument(\"-r\", \"--overwrite\", action=\"store_true\", help=\"Overwrite existing files\")\n        parser.set_defaults(search_parent=False, generator=\"builtin\")\n\n    def set_python(self, project: Project, python: str | None, hooks: HookManager) -> None:\n        from pdm.cli.commands.use import Command as UseCommand\n\n        python_info = UseCommand().do_use(\n            project,\n            python or \"\",\n            first=bool(python) or not self.interactive,\n            ignore_remembered=True,\n            ignore_requires_python=True,\n            save=False,\n            hooks=hooks,\n        )\n\n        if python_info.get_venv() is None:\n            project.core.ui.info(\n                \"You are using the PEP 582 mode, no virtualenv is created.\\n\"\n                \"You can change configuration with `pdm config python.use_venv True`.\\n\"\n                \"For more info, please visit https://peps.python.org/pep-0582/\"\n            )\n        project.python = python_info\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        if project.pyproject.exists():\n            project.core.ui.echo(\"pyproject.toml already exists, update it now.\", style=\"primary\")\n        else:\n            project.core.ui.echo(\"Creating a pyproject.toml for PDM...\", style=\"primary\")\n        self.set_interactive(not options.non_interactive and termui.is_interactive())\n        self.do_init(project, options=options)\n        project.core.ui.echo(\"Project is initialized successfully\", style=\"primary\")\n        if self.interactive:\n            actions.ask_for_import(project)\n"
  },
  {
    "path": "src/pdm/cli/commands/install.py",
    "content": "import argparse\nimport sys\nimport sysconfig\n\nfrom pdm import termui\nfrom pdm.cli import actions\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.filters import GroupSelection\nfrom pdm.cli.hooks import HookManager\nfrom pdm.cli.options import (\n    dry_run_option,\n    frozen_lockfile_option,\n    groups_group,\n    install_group,\n    lockfile_option,\n    override_option,\n    skip_option,\n    venv_option,\n)\nfrom pdm.project import Project\n\n\nclass Command(BaseCommand):\n    \"\"\"Install dependencies from lock file\"\"\"\n\n    arguments = (\n        *BaseCommand.arguments,\n        groups_group,\n        install_group,\n        override_option,\n        dry_run_option,\n        lockfile_option,\n        frozen_lockfile_option,\n        skip_option,\n        venv_option,\n    )\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\n            \"--check\",\n            action=\"store_true\",\n            help=\"Check if the lock file is up to date and fail otherwise\",\n        )\n        parser.add_argument(\"--plugins\", action=\"store_true\", help=\"Install the plugins specified in pyproject.toml\")\n\n    def install_plugins(self, project: Project) -> None:\n        from pdm.environments import PythonEnvironment\n        from pdm.installers.core import install_requirements\n        from pdm.models.requirements import parse_line\n\n        plugins = [parse_line(r) for r in project.pyproject.plugins]\n        if not plugins:\n            return\n        plugin_root = project.root / \".pdm-plugins\"\n        extra_paths = list({sysconfig.get_path(\"purelib\"), sysconfig.get_path(\"platlib\")})\n        environment = PythonEnvironment(\n            project, python=sys.executable, prefix=str(plugin_root), extra_paths=extra_paths\n        )\n        with project.core.ui.open_spinner(\"[success]Installing plugins...[/]\"):\n            with project.core.ui.logging(\"install-plugins\"):\n                install_requirements(\n                    plugins, environment, clean=True, use_install_cache=project.config[\"install.cache\"], allow_uv=False\n                )\n            if not plugin_root.joinpath(\".gitignore\").exists():\n                plugin_root.mkdir(exist_ok=True)\n                plugin_root.joinpath(\".gitignore\").write_text(\"*\\n\")\n        project.core.ui.echo(\"Plugins are installed successfully into [primary].pdm-plugins[/].\")\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        if not project.pyproject.is_valid and termui.is_interactive():\n            actions.ask_for_import(project)\n\n        if options.plugins:\n            return self.install_plugins(project)\n\n        hooks = HookManager(project, options.skip)\n\n        strategy = actions.check_lockfile(project, False)\n        selection = GroupSelection.from_options(project, options)\n        if strategy:\n            if options.check:\n                project.core.ui.echo(\n                    \"Please run [success]`pdm lock`[/] to update the lock file\",\n                    err=True,\n                )\n                sys.exit(1)\n            if project.enable_write_lockfile:\n                project.core.ui.echo(\"Updating the lock file...\", style=\"success\", err=True)\n            # We would like to keep the selected groups when the lockfile exists\n            # but use the groups passed-in when creating a new lockfile or doing a non-lock install.\n            if strategy == \"all\" or not project.enable_write_lockfile:\n                lock_selection = selection\n            else:\n                lock_selection = GroupSelection(project)\n            actions.do_lock(\n                project,\n                strategy=strategy,\n                dry_run=options.dry_run,\n                hooks=hooks,\n                groups=lock_selection.all(),\n            )\n\n        actions.do_sync(\n            project,\n            selection=selection,\n            no_editable=options.no_editable,\n            no_self=options.no_self or \"default\" not in selection,\n            dry_run=options.dry_run,\n            fail_fast=options.fail_fast,\n            hooks=hooks,\n        )\n"
  },
  {
    "path": "src/pdm/cli/commands/list.py",
    "content": "from __future__ import annotations\n\nimport argparse\nimport csv\nimport io\nimport json\nfrom collections import defaultdict\nfrom fnmatch import fnmatch\nfrom typing import Iterable, Mapping, Sequence\n\nfrom pdm.cli import actions\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.options import venv_option\nfrom pdm.cli.utils import (\n    DirectedGraph,\n    PackageNode,\n    build_dependency_graph,\n    check_project_file,\n    get_dist_location,\n    normalize_pattern,\n    show_dependency_graph,\n)\nfrom pdm.compat import importlib_metadata as im\nfrom pdm.exceptions import PdmUsageError\nfrom pdm.models.requirements import Requirement\nfrom pdm.project import Project\n\n# Group label for subdependencies\nSUBDEP_GROUP_LABEL = \":sub\"\n\n\nclass Command(BaseCommand):\n    \"\"\"List packages installed in the current working set\"\"\"\n\n    DEFAULT_FIELDS = \"name,version,location\"\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        venv_option.add_to_parser(parser)\n        graph = parser.add_mutually_exclusive_group()\n\n        parser.add_argument(\n            \"--freeze\",\n            action=\"store_true\",\n            help=\"Show the installed dependencies in pip's requirements.txt format\",\n        )\n\n        graph.add_argument(\"--tree\", \"--graph\", action=\"store_true\", help=\"Display a tree of dependencies\", dest=\"tree\")\n        parser.add_argument(\"-r\", \"--reverse\", action=\"store_true\", help=\"Reverse the dependency tree\")\n\n        parser.add_argument(\n            \"--resolve\",\n            action=\"store_true\",\n            default=False,\n            help=\"Resolve all requirements to output licenses (instead of just showing those currently installed)\",\n        )\n\n        parser.add_argument(\n            \"--fields\",\n            default=Command.DEFAULT_FIELDS,\n            help=\"Select information to output as a comma separated string. \"\n            f\"All fields: {','.join(sorted(Listable.KEYS))}.\",\n        )\n\n        parser.add_argument(\n            \"--sort\",\n            default=None,\n            help=\"Sort the output using a given field name. If nothing is \"\n            \"set, no sort is applied. Multiple fields can be combined with ','.\",\n        )\n\n        list_formats = parser.add_mutually_exclusive_group()\n\n        list_formats.add_argument(\n            \"--csv\",\n            action=\"store_true\",\n            help=\"Output dependencies in CSV document format\",\n        )\n\n        list_formats.add_argument(\n            \"--json\",\n            action=\"store_true\",\n            help=\"Output dependencies in JSON document format\",\n        )\n\n        list_formats.add_argument(\n            \"--markdown\",\n            action=\"store_true\",\n            help=\"Output dependencies and legal notices in markdown document format - best effort basis\",\n        )\n\n        parser.add_argument(\n            \"--include\",\n            default=\"\",\n            help=\"Dependency groups to include in the output. By default all are included\",\n        )\n\n        parser.add_argument(\n            \"--exclude\",\n            default=\"\",\n            help=\"Exclude dependency groups from the output\",\n        )\n        parser.add_argument(\n            \"patterns\",\n            nargs=\"*\",\n            help=\"Filter packages by patterns. e.g. pdm list requests-* flask-*. \"\n            \"In --tree mode, only show the subtree of the matched packages.\",\n            type=normalize_pattern,\n        )\n\n    @staticmethod\n    def filter_by_patterns(\n        packages: Mapping[str, im.Distribution], patterns: Iterable[str]\n    ) -> Mapping[str, im.Distribution]:\n        \"\"\"Filter packages by patterns\"\"\"\n        if not patterns:\n            return packages\n        return {k: v for k, v in packages.items() if any(fnmatch(k, p) for p in patterns)}\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        # Raise an error if the project is not defined.\n        check_project_file(project)\n\n        # Freeze.\n        if options.freeze:\n            self.handle_freeze(project, options)\n            return\n\n        # Map dependency groups to requirements.\n        name_to_groups: Mapping[str, set[str]] = defaultdict(set)\n        all_deps = project._resolve_dependencies()\n        for g in project.iter_groups():\n            for r in project.get_dependencies(g, all_deps):\n                k = r.key or \"unknown\"\n                name_to_groups[k].add(g)\n\n        # Set up `--include` and `--exclude` dep groups.\n        # Include everything by default (*) then exclude after.\n        # Check to make sure that only valid dep group names are given.\n        valid_groups = {*list(project.iter_groups()), SUBDEP_GROUP_LABEL}\n        include = set(parse_comma_separated_string(options.include, lowercase=False, asterisk_values=valid_groups))\n        if not all(g in valid_groups for g in include):\n            raise PdmUsageError(f\"--include groups must be selected from: {valid_groups}\")\n        exclude = set(parse_comma_separated_string(options.exclude, lowercase=False, asterisk_values=valid_groups))\n        if exclude and not all(g in valid_groups for g in exclude):\n            raise PdmUsageError(f\"--exclude groups must be selected from: {valid_groups}\")\n\n        # Include selects only certain groups when set, but always selects :sub\n        # unless it is explicitly unset.\n        selected_groups = include if include else valid_groups\n        selected_groups = selected_groups | {SUBDEP_GROUP_LABEL}\n        selected_groups = selected_groups - (exclude - include)\n\n        # Requirements as importtools distributions (eg packages).\n        # Resolve all the requirements. Map the candidates to distributions.\n        requirements = [r for g in selected_groups if g != SUBDEP_GROUP_LABEL for r in project.get_dependencies(g)]\n        if options.resolve:\n            candidates = actions.resolve_candidates_from_lockfile(\n                project, requirements, groups=selected_groups - {SUBDEP_GROUP_LABEL}\n            )\n            packages: Mapping[str, im.Distribution] = {\n                k: c.prepare(project.environment).metadata for k, c in candidates.items()\n            }\n\n        # Use requirements from the working set (currently installed).\n        else:\n            packages = project.environment.get_working_set()\n\n        selected_keys = {r.identify() for r in requirements}\n        dep_graph = build_dependency_graph(\n            packages,\n            project.environment.spec,\n            None if not (include or exclude) else selected_keys,\n            include_sub=SUBDEP_GROUP_LABEL in selected_groups,\n        )\n\n        # Process as a graph or list.\n        if options.tree:\n            self.handle_graph(dep_graph, project, options)\n        else:\n            selected_packages = [k.name.split(\"[\")[0] for k in dep_graph if k]\n            packages = self.filter_by_patterns(\n                {k: v for k, v in packages.items() if k in selected_packages}, options.patterns\n            )\n            self.handle_list(packages, name_to_groups, project, options)\n\n    def handle_freeze(self, project: Project, options: argparse.Namespace) -> None:\n        if options.tree:\n            raise PdmUsageError(\"--tree cannot be used with --freeze\")\n        if options.reverse:\n            raise PdmUsageError(\"--reverse cannot be used without --tree\")\n        if options.fields != Command.DEFAULT_FIELDS:\n            raise PdmUsageError(\"--fields cannot be used with --freeze\")\n        if options.resolve:\n            raise PdmUsageError(\"--resolve cannot be used with --freeze\")\n        if options.sort:\n            raise PdmUsageError(\"--sort cannot be used with --freeze\")\n        if options.csv:\n            raise PdmUsageError(\"--csv cannot be used with --freeze\")\n        if options.json:\n            raise PdmUsageError(\"--json cannot be used with --freeze\")\n        if options.markdown:\n            raise PdmUsageError(\"--markdown cannot be used with --freeze\")\n        if options.include or options.exclude:\n            raise PdmUsageError(\"--include/--exclude cannot be used with --freeze\")\n\n        working_set = self.filter_by_patterns(project.environment.get_working_set(), options.patterns)\n        requirements = sorted(\n            (Requirement.from_dist(dist).as_line() for dist in working_set.values()),\n            key=lambda x: x.lower(),\n        )\n        project.core.ui.echo(\"\\n\".join(requirements))\n\n    def handle_graph(\n        self,\n        dep_graph: DirectedGraph[PackageNode | None],\n        project: Project,\n        options: argparse.Namespace,\n    ) -> None:\n        if options.csv:\n            raise PdmUsageError(\"--csv cannot be used with --tree\")\n        if options.markdown:\n            raise PdmUsageError(\"--markdown cannot be used with --tree\")\n        if options.sort:\n            raise PdmUsageError(\"--sort cannot be used with --tree\")\n\n        show_dependency_graph(project, dep_graph, reverse=options.reverse, json=options.json, patterns=options.patterns)\n\n    def handle_list(\n        self,\n        packages: Mapping[str, im.Distribution],\n        name_to_groups: Mapping[str, set[str]],\n        project: Project,\n        options: argparse.Namespace,\n    ) -> None:\n        if options.reverse:\n            raise PdmUsageError(\"--reverse cannot be used without --tree\")\n\n        # Check the fields are specified OK.\n        fields = parse_comma_separated_string(options.fields, asterisk_values=Listable.KEYS)\n        if not all(field in Listable.KEYS for field in fields):\n            raise PdmUsageError(f\"--fields must specify one or more of: {Listable.KEYS}\")\n\n        # Wrap each distribution with a Listable (and a groups pairing)\n        # to make it easier to filter on later.\n        def _group_of(name: str) -> set[str]:\n            return name_to_groups.get(name, {SUBDEP_GROUP_LABEL})\n\n        records = [Listable(d, _group_of(k)) for k, d in packages.items()]\n        ui = project.core.ui\n\n        # Order based on a field key.\n        keys = parse_comma_separated_string(options.sort) if options.sort else [\"name\"]\n        if not all(key in Listable.KEYS for key in keys):\n            raise PdmUsageError(f\"--sort key must be one of: {','.join(Listable.KEYS)}\")\n        records.sort(key=lambda d: tuple(d[key].casefold() for key in keys))\n\n        # Write CSV\n        if options.csv:\n            buffer = io.StringIO()\n            writer = csv.DictWriter(buffer, fieldnames=fields)\n            writer.writeheader()\n            for row in records:\n                writer.writerow(row.json(fields))\n            ui.echo(buffer.getvalue(), highlight=True, end=\"\")\n\n        # Write JSON\n        elif options.json:\n            json_row = [row.json(fields) for row in records]\n            ui.echo(json.dumps(json_row, indent=4), highlight=True)\n\n        # Write Markdown\n        elif options.markdown:\n            body = [f\"# {project.name} licenses\"]\n            body.extend(row.markdown(fields) for row in records)\n            text_body = \"\\n\".join(body)\n            try:\n                ui.echo(text_body, highlight=True)\n            except UnicodeEncodeError:\n                ui.error(\n                    \"Markdown output contains non-ASCII characters. \"\n                    \"Setting env var PYTHONIOENCODING to 'utf8' may fix this.\",\n                )\n                ui.echo(text_body.encode().decode(\"ascii\", errors=\"ignore\"), highlight=True)\n                ui.echo(\"**Problem decoding file as UTF-8.  Some characters may be omit.**\")\n\n        # Write nice table format.\n        else:\n            formatted = [row.rich(fields) for row in records]\n            ui.display_columns(formatted, fields)\n\n\ndef parse_comma_separated_string(\n    comma_string: str,\n    lowercase: bool = True,\n    asterisk_values: Iterable[str] | None = None,\n) -> list[str]:\n    \"\"\"Parse a CLI comma separated string.\n    Apply optional lowercase transformation and if the value given is \"*\" then\n    return a list of pre-defined values (`asterisk_values`).\n    \"\"\"\n    if asterisk_values is not None and comma_string.strip() == \"*\":\n        return list(asterisk_values)\n    items = f\"{comma_string}\".split(\",\")\n    items = [el.strip() for el in items if el]\n    if lowercase:\n        items = [el.lower() for el in items]\n    return items\n\n\nclass Listable:\n    \"\"\"Wrapper makes sorting and exporting information about a Distribution\n    easier.  It also retrieves license information from dist-info metadata.\n\n    https://packaging.python.org/en/latest/specifications/core-metadata\n    \"\"\"\n\n    # Fields that users are allowed to sort on.\n    KEYS = frozenset([\"name\", \"groups\", \"version\", \"homepage\", \"licenses\", \"location\"])\n\n    def __init__(self, dist: im.Distribution, groups: set[str]):\n        self.dist = dist\n\n        self.name = dist.metadata.get(\"Name\")\n        self.groups = \"|\".join(groups)\n\n        self.version = dist.metadata.get(\"Version\")\n        self.version = None if self.version == \"UNKNOWN\" else self.version\n\n        self.homepage = dist.metadata.get(\"Home-Page\")\n        self.homepage = None if self.homepage == \"UNKNOWN\" else self.homepage\n\n        # If the License metadata field is empty or UNKNOWN then try to\n        # find the license in the Trove classifiers.  There may be more than one\n        # so generate a pipe separated list (to avoid complexity with CSV export).\n        self.licenses = dist.metadata.get(\"License\")\n        self.licenses = None if self.licenses == \"UNKNOWN\" else self.licenses\n\n        # Sometimes package metadata contains the full license text.\n        # e.g. license = { file=\"LICENSE\" } in pyproject.toml\n        # To identify this, check for newlines or very long strings.\n        # 50 chars is picked because the longest OSI license (WTFPL) full name is 43 characters.\n        is_full_text = (self.licenses and \"\\n\" in self.licenses) or len(self.licenses or \"\") > 50\n\n        # If that is the case, look at the classifiers instead.\n        if not self.licenses or is_full_text:\n            classifier_licenses = [v for v in dist.metadata.get_all(\"Classifier\", []) if v.startswith(\"License\")]\n            alternatives = [parts.split(\"::\") for parts in classifier_licenses]\n            alternatives = [part[-1].strip() for part in alternatives if part]\n            self.licenses = \"|\".join(alternatives)\n\n    @property\n    def location(self) -> str:\n        return get_dist_location(self.dist)\n\n    def license_files(self) -> list[im.PackagePath]:\n        \"\"\"Path to files inside the package that may contain license information\n        or other legal notices.\n\n        The implementation is a \"best effort\" and may contain errors, select\n        incorrect information, or otherwise be error-prone. It is not a\n        substitute for a lawyer.\n        \"\"\"\n        if not self.dist.files:\n            return []\n\n        # Inconsistency between packages means that we check in several locations\n        # for license files.  There may be 0 or more of these.  There may be false\n        # positives & negatives.\n        locations = (\"**/LICENSE*\", \"**/LICENCE*\", \"**/COPYING*\", \"**/NOTICE*\")\n\n        # Compile a list of all file paths in the distribution that look like\n        # they might contain a license file.\n        paths = []\n        for path in self.dist.files:\n            paths += [path for loc in locations if path.match(loc)]\n        return paths\n\n    def __getitem__(self, field: str) -> str:\n        if field not in Listable.KEYS:\n            raise PdmUsageError(f\"list field `{field}` not in: {Listable.KEYS}\")\n        return getattr(self, field)\n\n    def json(self, fields: Sequence[str]) -> dict:\n        return {f: self[f] for f in fields}\n\n    def rich(self, fields: Sequence[str]) -> Sequence[str]:\n        output = []\n        for field in fields:\n            data = f\"{self[field]}\"\n            data = data if field != \"name\" else f\"[req]{data}[/]\"\n            data = data if field != \"version\" else f\"[warning]{data}[/]\"\n            data = data if field != \"groups\" else f\"[error]{data}[/]\"\n            output.append(data)\n        return output\n\n    def markdown(self, fields: Sequence[str]) -> str:\n        nl = \"\\n\"\n        section = \"\"\n\n        # Heading\n        section += f\"## {self.name}{nl}\"\n        section += f\"{nl}\"\n\n        # Table\n        section += f\"| Name | {self.name} |{nl}\"\n        section += f\"|----|----|{nl}\"\n        for field in fields:\n            if field == \"name\":\n                continue\n            section += f\"| {field.capitalize()} | {self[field]} |{nl}\"\n        section += f\"{nl}\"\n\n        # Files\n        for path in self.license_files():\n            section += f\"{path}{nl}\"\n            section += f\"{nl}{nl}\"\n            section += f\"````{nl}\"\n            try:\n                section += path.read_text(\"utf-8\")\n            except UnicodeDecodeError:\n                section += \"Problem decoding file as UTF-8\"\n            except Exception as err:\n                section += f\"Problem finding license text: {err}\"\n            section += f\"{nl}\"\n            section += f\"````{nl}\"\n            section += f\"{nl}\"\n        return section\n"
  },
  {
    "path": "src/pdm/cli/commands/lock.py",
    "content": "import argparse\nimport re\nimport sys\nfrom typing import cast\n\nfrom pdm import termui\nfrom pdm.cli import actions\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.filters import GroupSelection\nfrom pdm.cli.hooks import HookManager\nfrom pdm.cli.options import (\n    config_setting_option,\n    groups_group,\n    lock_strategy_group,\n    lockfile_option,\n    no_isolation_option,\n    override_option,\n    skip_option,\n)\nfrom pdm.models.markers import EnvSpec\nfrom pdm.models.specifiers import PySpecSet\nfrom pdm.project import Project\nfrom pdm.utils import convert_to_datetime\n\n\nclass Command(BaseCommand):\n    \"\"\"Resolve and lock dependencies\"\"\"\n\n    arguments = (\n        *BaseCommand.arguments,\n        lockfile_option,\n        no_isolation_option,\n        config_setting_option,\n        override_option,\n        skip_option,\n        groups_group,\n        lock_strategy_group,\n    )\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\n            \"--refresh\",\n            action=\"store_true\",\n            help=\"Refresh the content hash and file hashes in the lock file\",\n        )\n\n        parser.add_argument(\n            \"--check\",\n            action=\"store_true\",\n            help=\"Check if the lock file is up to date and quit\",\n        )\n        parser.add_argument(\n            \"--update-reuse\",\n            action=\"store_const\",\n            dest=\"update_strategy\",\n            default=\"all\",\n            const=\"reuse\",\n            help=\"Reuse pinned versions already present in lock file if possible\",\n        )\n        parser.add_argument(\n            \"--update-reuse-installed\",\n            action=\"store_const\",\n            dest=\"update_strategy\",\n            const=\"reuse-installed\",\n            help=\"Reuse installed packages if possible\",\n        )\n        parser.add_argument(\n            \"--exclude-newer\",\n            help=\"Exclude packages newer than the given UTC date in format `YYYY-MM-DD[THH:MM:SSZ]`\",\n            type=convert_to_datetime,\n        )\n\n        target_group = parser.add_argument_group(\"Lock Target\")\n        target_group.add_argument(\"--python\", help=\"The Python range to lock for. E.g. `>=3.9`, `==3.12.*`\")\n        target_group.add_argument(\n            \"--platform\",\n            help=\"The platform to lock for. E.g. `windows`, `linux`, `macos`, `manylinux_2_17_x86_64`. \"\n            \"See docs for available choices: http://pdm-project.org/en/latest/usage/lock-targets/\",\n        )\n        target_group.add_argument(\n            \"--implementation\",\n            help=\"The Python implementation to lock for. E.g. `cpython`, `pypy`, `pyston`\",\n        )\n        target_group.add_argument(\"--append\", action=\"store_true\", help=\"Append the result to the current lock file\")\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        if options.check:\n            strategy = actions.check_lockfile(project, False)\n            if strategy:\n                project.core.ui.echo(\n                    f\"[error]{termui.Emoji.FAIL}[/] Lockfile is [error]out of date[/].\",\n                    err=True,\n                    verbosity=termui.Verbosity.DETAIL,\n                )\n                sys.exit(1)\n            else:\n                project.core.ui.echo(\n                    f\"[success]{termui.Emoji.SUCC}[/] Lockfile is [success]up to date[/].\",\n                    err=True,\n                    verbosity=termui.Verbosity.DETAIL,\n                )\n                sys.exit(0)\n        selection = GroupSelection.from_options(project, options)\n        strategy = options.update_strategy\n        if options.exclude_newer:\n            strategy = \"all\"\n            if strategy != options.update_strategy:\n                project.core.ui.info(\"--exclude-newer is set, forcing --update-all\")\n        project.core.state.exclude_newer = options.exclude_newer\n        env_spec: EnvSpec | None = None\n        if any([options.python, options.platform, options.implementation]):\n            replace_dict = {}\n            if options.python:\n                if re.match(r\"[\\d.]+\", options.python):\n                    options.python = f\">={options.python}\"\n                replace_dict[\"requires_python\"] = PySpecSet(options.python)\n            if options.platform:\n                replace_dict[\"platform\"] = options.platform\n            if options.implementation:\n                replace_dict[\"implementation\"] = options.implementation\n            env_spec = project.environment.allow_all_spec.replace(**replace_dict)\n\n        actions.do_lock(\n            project,\n            refresh=options.refresh,\n            strategy=cast(str, strategy),\n            groups=selection.all(),\n            strategy_change=options.strategy_change,\n            hooks=HookManager(project, options.skip),\n            env_spec=env_spec,\n            append=options.append,\n        )\n"
  },
  {
    "path": "src/pdm/cli/commands/new.py",
    "content": "import argparse\nimport os\n\nfrom pdm.cli.commands.base import verbose_option\nfrom pdm.cli.commands.init import Command as InitCommand\nfrom pdm.project.core import Project\n\n\nclass Command(InitCommand):\n    \"\"\"Create a new Python project at <project_path>\"\"\"\n\n    supports_other_generator = False\n\n    arguments = (verbose_option,)\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        super().add_arguments(parser)\n        parser.add_argument(\"project_path\", help=\"The path to create the new project\")\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        new_project = project.core.create_project(\n            options.project_path, global_config=options.config or os.getenv(\"PDM_CONFIG_FILE\")\n        )\n        return super().handle(new_project, options)\n"
  },
  {
    "path": "src/pdm/cli/commands/outdated.py",
    "content": "from __future__ import annotations\n\nimport functools\nimport json\nfrom concurrent.futures import ThreadPoolExecutor\nfrom dataclasses import asdict, dataclass\nfrom fnmatch import fnmatch\nfrom itertools import zip_longest\nfrom typing import TYPE_CHECKING, cast\n\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.utils import normalize_pattern\nfrom pdm.models.requirements import strip_extras\nfrom pdm.utils import normalize_name\n\nif TYPE_CHECKING:\n    from argparse import ArgumentParser, Namespace\n\n    from unearth import PackageFinder\n\n    from pdm.project.core import Project\n\n\n@dataclass\nclass ListPackage:\n    package: str\n    groups: list[str]\n    installed_version: str\n    pinned_version: str\n    latest_version: str = \"\"\n\n\n@functools.lru_cache\ndef _find_first_diff(a: str, b: str) -> int:\n    a_parts = a.split(\".\")\n    b_parts = b.split(\".\")\n    for i, (x, y) in enumerate(zip_longest(a_parts, b_parts)):\n        if x != y:\n            return (len(\".\".join(a_parts[:i])) + 1) if i > 0 else 0\n    return 0\n\n\nclass Command(BaseCommand):\n    \"\"\"Check for outdated packages and list the latest versions on indexes.\"\"\"\n\n    def add_arguments(self, parser: ArgumentParser) -> None:\n        parser.add_argument(\n            \"--json\", action=\"store_const\", const=\"json\", dest=\"format\", default=\"table\", help=\"Output in JSON format\"\n        )\n        parser.add_argument(\"--include-sub\", action=\"store_true\", help=\"Include sub-dependencies\")\n        parser.add_argument(\"patterns\", nargs=\"*\", help=\"The packages to check\", type=normalize_pattern)\n\n    @staticmethod\n    def _match_pattern(name: str, patterns: list[str]) -> bool:\n        return not patterns or any(fnmatch(name, p) for p in patterns)\n\n    @staticmethod\n    def _populate_latest_version(finder: PackageFinder, package: ListPackage) -> None:\n        best = finder.find_best_match(package.package).best\n        if best:\n            package.latest_version = best.version or \"\"\n\n    @staticmethod\n    def _format_json(packages: list[ListPackage]) -> str:\n        return json.dumps([asdict(package) for package in packages], indent=2)\n\n    @staticmethod\n    def _render_version(version: str, base_version: str) -> str:\n        from packaging.version import InvalidVersion\n\n        from pdm.utils import parse_version\n\n        if not version or version == base_version:\n            return version\n        if not base_version:\n            return f\"[bold red]{version}[/]\"\n\n        try:\n            parsed_version = parse_version(version)\n            parsed_base_version = parse_version(base_version)\n        except InvalidVersion:\n            return version\n        first_diff = _find_first_diff(version, base_version)\n        head, tail = version[:first_diff], version[first_diff:]\n        if parsed_version.major != parsed_base_version.major:\n            return f\"{head}[bold red]{tail}[/]\"\n        if parsed_version.minor != parsed_base_version.minor:\n            return f\"{head}[bold yellow]{tail}[/]\"\n        return f\"{head}[bold green]{tail}[/]\"\n\n    def handle(self, project: Project, options: Namespace) -> None:\n        environment = project.environment\n        installed = environment.get_working_set()\n        resolved = {strip_extras(k)[0]: v for k, v in project.get_locked_repository().candidates.items()}\n\n        collected: list[ListPackage] = []\n        project_dependencies: dict[str, list[str]] = {}\n        for group, dependencies in project.all_dependencies.items():\n            for dep in dependencies:\n                project_dependencies.setdefault(cast(str, dep.key), []).append(group)\n\n        for name, distribution in installed.items():\n            if not self._match_pattern(name, options.patterns):\n                continue\n            if project.name and name == normalize_name(project.name):\n                continue\n            constrained_version = resolved.pop(name).version or \"\" if name in resolved else \"\"\n            if not options.include_sub and name not in project_dependencies:\n                continue\n            groups = project_dependencies.get(name, [])\n            collected.append(ListPackage(name, groups, distribution.version or \"\", constrained_version))\n\n        for name, candidate in resolved.items():\n            if not self._match_pattern(name, options.patterns):\n                continue\n            if candidate.req.marker and not candidate.req.marker.matches(environment.spec):\n                continue\n            if not options.include_sub and name not in project_dependencies:\n                continue\n            groups = project_dependencies.get(name, [])\n            collected.append(ListPackage(name, groups, \"\", candidate.version or \"\"))\n\n        with environment.get_finder() as finder, ThreadPoolExecutor() as executor:\n            for package in collected:\n                executor.submit(self._populate_latest_version, finder, package)\n\n        collected = sorted(\n            [p for p in collected if p.latest_version and p.latest_version != p.installed_version],\n            key=lambda p: p.package,\n        )\n        if options.format == \"json\":\n            print(self._format_json(collected))\n        else:\n            rows = [\n                (\n                    f\"[bold]{package.package}[/]\",\n                    \", \".join(package.groups),\n                    package.installed_version,\n                    self._render_version(package.pinned_version, package.installed_version),\n                    self._render_version(package.latest_version, package.installed_version),\n                )\n                for package in collected\n            ]\n            project.core.ui.display_columns(rows, header=[\"Package\", \"Groups\", \"Installed\", \"Pinned\", \"Latest\"])\n"
  },
  {
    "path": "src/pdm/cli/commands/publish/__init__.py",
    "content": "from __future__ import annotations\n\nimport argparse\nimport os\nfrom typing import TYPE_CHECKING\n\nfrom pdm.cli.commands import build\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.commands.publish.package import PackageFile\nfrom pdm.cli.hooks import HookManager\nfrom pdm.cli.options import project_option, skip_option, verbose_option\nfrom pdm.exceptions import PdmUsageError, PublishError\nfrom pdm.termui import logger\n\nif TYPE_CHECKING:\n    from httpx import Response\n\n    from pdm.cli.commands.publish.repository import Repository\n    from pdm.project import Project\n\n\nclass Command(BaseCommand):\n    \"\"\"Build and publish the project to PyPI\"\"\"\n\n    arguments = (verbose_option, project_option, skip_option)\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\n            \"-r\",\n            \"--repository\",\n            help=\"The repository name or url to publish the package to [env var: PDM_PUBLISH_REPO]\",\n        )\n        parser.add_argument(\n            \"-u\",\n            \"--username\",\n            help=\"The username to access the repository [env var: PDM_PUBLISH_USERNAME]\",\n        )\n        parser.add_argument(\n            \"-P\",\n            \"--password\",\n            help=\"The password to access the repository [env var: PDM_PUBLISH_PASSWORD]\",\n        )\n        parser.add_argument(\n            \"-S\",\n            \"--sign\",\n            action=\"store_true\",\n            help=\"Upload the package with PGP signature\",\n        )\n        parser.add_argument(\n            \"-i\",\n            \"--identity\",\n            help=\"GPG identity used to sign files.\",\n        )\n        parser.add_argument(\n            \"-c\",\n            \"--comment\",\n            help=\"The comment to include with the distribution file.\",\n        )\n        parser.add_argument(\n            \"--no-build\",\n            action=\"store_false\",\n            dest=\"build\",\n            help=\"Don't build the package before publishing\",\n        )\n        parser.add_argument(\n            \"-d\",\n            \"--dest\",\n            help=\"The directory to upload the package from\",\n            default=\"dist\",\n        )\n        parser.add_argument(\n            \"--skip-existing\",\n            action=\"store_true\",\n            help=\"Skip uploading files that already exist. This may not work with some repository implementations.\",\n        )\n        group = parser.add_mutually_exclusive_group()\n        group.add_argument(\n            \"--no-very-ssl\", action=\"store_false\", dest=\"verify_ssl\", help=\"Disable SSL verification\", default=None\n        )\n        group.add_argument(\n            \"--ca-certs\",\n            dest=\"ca_certs\",\n            help=\"The path to a PEM-encoded Certificate Authority bundle to use\"\n            \" for publish server validation [env var: PDM_PUBLISH_CA_CERTS]\",\n        )\n\n    @staticmethod\n    def _make_package(filename: str, signatures: dict[str, str], options: argparse.Namespace) -> PackageFile:\n        p = PackageFile.from_filename(filename, options.comment)\n        if p.base_filename in signatures:\n            p.add_gpg_signature(signatures[p.base_filename], p.base_filename + \".asc\")\n        elif options.sign:\n            p.sign(options.identity)\n        return p\n\n    @staticmethod\n    def _skip_upload(response: Response) -> bool:\n        status = response.status_code\n        reason = response.reason_phrase.lower()\n        text = response.text.lower()\n\n        # Borrowed from https://github.com/pypa/twine/blob/main/twine/commands/upload.py#L149\n        return (\n            # pypiserver (https://pypi.org/project/pypiserver)\n            status == 409\n            # PyPI / TestPyPI / GCP Artifact Registry\n            or (status == 400 and any(\"already exist\" in x for x in [reason, text]))\n            # Nexus Repository OSS (https://www.sonatype.com/nexus-repository-oss)\n            or (\n                status == 400\n                and any(token in x for x in [reason, text] for token in [\"updating asset\", \"cannot be updated\"])\n            )\n            # Artifactory (https://jfrog.com/artifactory/)\n            or (status == 403 and \"overwrite artifact\" in text)\n            # Gitlab Enterprise Edition (https://about.gitlab.com)\n            or (status == 400 and \"already been taken\" in text)\n        )\n\n    @staticmethod\n    def _check_response(response: Response) -> None:\n        import httpx\n\n        message = \"\"\n        if response.status_code == 410 and \"pypi.python.org\" in str(response.url):\n            message = (\n                \"Uploading to these sites is deprecated. \"\n                \"Try using https://upload.pypi.org/legacy/ \"\n                \"(or https://test.pypi.org/legacy/) instead.\"\n            )\n        elif response.status_code == 405 and \"pypi.org\" in str(response.url):\n            message = \"It appears you're trying to upload to pypi.org but have an invalid URL.\"\n        else:\n            try:\n                response.raise_for_status()\n            except httpx.HTTPStatusError as err:\n                message = str(err)\n                if response.text:\n                    logger.debug(response.text)\n        if message:\n            raise PublishError(message)\n\n    @staticmethod\n    def get_repository(project: Project, options: argparse.Namespace) -> Repository:\n        from pdm.cli.commands.publish.repository import Repository\n\n        repository = options.repository or os.getenv(\"PDM_PUBLISH_REPO\", \"pypi\")\n        username = options.username or os.getenv(\"PDM_PUBLISH_USERNAME\")\n        password = options.password or os.getenv(\"PDM_PUBLISH_PASSWORD\")\n        ca_certs = options.ca_certs or os.getenv(\"PDM_PUBLISH_CA_CERTS\")\n\n        config = project.project_config.get_repository_config(repository, \"repository\")\n        if config is None:\n            config = project.global_config.get_repository_config(repository, \"repository\")\n            if config is None:\n                raise PdmUsageError(f\"Missing repository config of {repository}\")\n        else:\n            global_config = project.global_config.get_repository_config(repository, \"repository\")\n            if global_config is not None:\n                config.passive_update(global_config)\n\n        assert config.url is not None\n        if username is not None:\n            config.username = username\n        if password is not None:\n            config.password = password\n        if ca_certs is not None:\n            config.ca_certs = ca_certs\n        if options.verify_ssl is False:\n            config.verify_ssl = options.verify_ssl\n        config.populate_keyring_auth()\n        return Repository(project, config)\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        hooks = HookManager(project, options.skip)\n\n        hooks.try_emit(\"pre_publish\")\n\n        if options.build:\n            build.Command.do_build(project, dest=options.dest, hooks=hooks)\n\n        upload_dir = project.root.joinpath(options.dest)\n        package_files = [str(p) for p in upload_dir.iterdir() if not p.name.endswith(\".asc\")]\n        signatures = {p.stem: str(p) for p in upload_dir.iterdir() if p.name.endswith(\".asc\")}\n\n        repository = self.get_repository(project, options)\n        uploaded: list[PackageFile] = []\n        with project.core.ui.logging(\"publish\"):\n            packages = sorted(\n                (self._make_package(p, signatures, options) for p in package_files),\n                # Upload wheels first if they exist.\n                key=lambda p: not p.base_filename.endswith(\".whl\"),\n            )\n            for package in packages:\n                resp = repository.upload(package)\n                logger.debug(\"Response from %s:\\n%s %s\", resp.url, resp.status_code, resp.reason_phrase)\n\n                if options.skip_existing and self._skip_upload(resp):\n                    project.core.ui.warn(f\"Skipping {package.base_filename} because it appears to already exist\")\n                    continue\n                self._check_response(resp)\n                uploaded.append(package)\n\n        release_urls = repository.get_release_urls(uploaded)\n        if release_urls:\n            project.core.ui.echo(\"\\n[success]View at:\")\n            for url in release_urls:\n                project.core.ui.echo(url)\n\n        hooks.try_emit(\"post_publish\")\n"
  },
  {
    "path": "src/pdm/cli/commands/publish/package.py",
    "content": "from __future__ import annotations\n\nimport email\nimport email.message\nimport email.policy\nimport hashlib\nimport io\nimport os\nimport re\nimport subprocess\nfrom dataclasses import dataclass\nfrom typing import IO, Any, cast\n\nfrom pdm.exceptions import PdmUsageError, ProjectError\nfrom pdm.termui import logger\n\nDIST_EXTENSIONS = {\n    \".whl\": \"bdist_wheel\",\n    \".tar.bz2\": \"sdist\",\n    \".tar.gz\": \"sdist\",\n    \".zip\": \"sdist\",\n}\nwheel_file_re = re.compile(\n    r\"\"\"^(?P<namever>(?P<name>.+?)(-(?P<ver>\\d.+?))?)\n        ((-(?P<build>\\d.*?))?-(?P<pyver>.+?)-(?P<abi>.+?)-(?P<plat>.+?)\n        \\.whl|\\.dist-info)$\"\"\",\n    re.VERBOSE,\n)\nUTF8_POLICY = email.policy.EmailPolicy(utf8=True)\n\n\ndef parse_metadata(fp: IO[bytes]) -> email.message.Message:\n    \"\"\"\n    Note that this function will close fp. See https://github.com/python/cpython/issues/65562.\n    \"\"\"\n    with io.TextIOWrapper(fp, encoding=\"utf-8\", errors=\"surrogateescape\") as file:\n        return email.message_from_file(file, policy=UTF8_POLICY)  # type: ignore[arg-type]\n\n\n@dataclass\nclass PackageFile:\n    \"\"\"A distribution file for upload.\n\n    XXX: currently only supports sdist and wheel.\n    \"\"\"\n\n    filename: str\n    metadata: email.message.Message\n    comment: str | None\n    py_version: str | None\n    filetype: str\n\n    def __post_init__(self) -> None:\n        self.base_filename = os.path.basename(self.filename)\n        self.gpg_signature: tuple[str, bytes] | None = None\n\n    def get_hashes(self) -> dict[str, str]:\n        hashers = {\"sha256_digest\": hashlib.sha256()}\n        try:\n            hashers[\"md5_digest\"] = hashlib.md5()\n        except ValueError:\n            pass\n        try:\n            hashers[\"blake2_256_digest\"] = hashlib.blake2b(digest_size=256 // 8)  # type: ignore[assignment]\n        except (TypeError, ValueError):\n            pass\n        with open(self.filename, \"rb\") as f:\n            for chunk in iter(lambda: f.read(8192), b\"\"):\n                for hasher in hashers.values():\n                    hasher.update(chunk)\n        return {k: v.hexdigest() for k, v in hashers.items()}\n\n    @classmethod\n    def from_filename(cls, filename: str, comment: str | None) -> PackageFile:\n        filetype = \"\"\n        for ext, dtype in DIST_EXTENSIONS.items():\n            if filename.endswith(ext):\n                filetype = dtype\n                break\n        else:\n            raise PdmUsageError(f\"Unknown distribution file type: {filename}\")\n        if filetype == \"bdist_wheel\":\n            metadata = cls.read_metadata_from_wheel(filename)\n            match = wheel_file_re.match(os.path.basename(filename))\n            if match is None:\n                py_ver = \"any\"\n            else:\n                py_ver = match.group(\"pyver\")\n        elif filename.endswith(\".zip\"):\n            metadata = cls.read_metadata_from_zip(filename)\n            py_ver = \"source\"\n        else:\n            metadata = cls.read_metadata_from_tar(filename)\n            py_ver = \"source\"\n        return cls(filename, metadata, comment, py_ver, filetype)\n\n    @staticmethod\n    def read_metadata_from_tar(filename: str) -> email.message.Message:\n        import tarfile\n\n        from unearth.preparer import has_leading_dir, split_leading_dir\n\n        if filename.endswith(\".gz\"):\n            mode = \"r:gz\"\n        elif filename.endswith(\".bz2\"):\n            mode = \"r:bz2\"\n        else:\n            logger.warning(f\"Can't determine the compression mode for {filename}\")\n            mode = \"r:*\"\n        with tarfile.open(filename, mode) as tar:  # type: ignore[call-overload]\n            members = tar.getmembers()\n            has_leading = has_leading_dir(m.name for m in members)\n            for m in members:\n                fn = split_leading_dir(m.name)[1] if has_leading else m.name\n                if fn == \"PKG-INFO\":\n                    return parse_metadata(cast(IO[bytes], tar.extractfile(m)))\n        raise ProjectError(f\"No PKG-INFO found in {filename}\")\n\n    @staticmethod\n    def read_metadata_from_zip(filename: str) -> email.message.Message:\n        import zipfile\n\n        from unearth.preparer import has_leading_dir, split_leading_dir\n\n        with zipfile.ZipFile(filename, allowZip64=True) as zip:\n            filenames = zip.namelist()\n            has_leading = has_leading_dir(filenames)\n            for name in filenames:\n                fn = split_leading_dir(name)[1] if has_leading else name\n                if fn == \"PKG-INFO\":\n                    return parse_metadata(zip.open(name))\n        raise ProjectError(f\"No PKG-INFO found in {filename}\")\n\n    @staticmethod\n    def read_metadata_from_wheel(filename: str) -> email.message.Message:\n        import zipfile\n\n        with zipfile.ZipFile(filename, allowZip64=True) as zip:\n            for fn in zip.namelist():\n                if fn.replace(\"\\\\\", \"/\").endswith(\".dist-info/METADATA\"):\n                    return parse_metadata(zip.open(fn))\n        raise ProjectError(f\"No egg-info is found in {filename}\")\n\n    def add_gpg_signature(self, filename: str, signature_name: str) -> None:\n        if self.gpg_signature is not None:\n            raise PdmUsageError(\"GPG signature already added\")\n        with open(filename, \"rb\") as f:\n            self.gpg_signature = (signature_name, f.read())\n\n    def sign(self, identity: str | None) -> None:\n        logger.info(\"Signing %s with gpg\", self.base_filename)\n        gpg_args = [\"gpg\", \"--detach-sign\"]\n        if identity is not None:\n            gpg_args.extend([\"--local-user\", identity])\n        gpg_args.extend([\"-a\", self.filename])\n        self._run_gpg(gpg_args)\n        self.add_gpg_signature(self.filename + \".asc\", self.base_filename + \".asc\")\n\n    @staticmethod\n    def _run_gpg(gpg_args: list[str]) -> None:\n        try:\n            subprocess.run(gpg_args, check=True)\n            return\n        except FileNotFoundError:\n            logger.warning(\"gpg executable not available. Attempting fallback to gpg2.\")\n\n        gpg_args[0] = \"gpg2\"\n        try:\n            subprocess.run(gpg_args, check=True)\n        except FileNotFoundError:\n            raise PdmUsageError(\n                \"'gpg' or 'gpg2' executables not available.\\n\"\n                \"Try installing one of these or specifying an executable \"\n                \"with the --sign-with flag.\"\n            ) from None\n\n    @property\n    def metadata_dict(self) -> dict[str, Any]:\n        meta = self.metadata\n        data = {\n            # identify release\n            \"name\": meta[\"Name\"],\n            \"version\": meta[\"Version\"],\n            # file content\n            \"filetype\": self.filetype,\n            \"pyversion\": self.py_version,\n            # additional meta-data\n            \"metadata_version\": meta[\"Metadata-Version\"],\n            \"summary\": meta[\"Summary\"],\n            \"home_page\": meta[\"Home-page\"],\n            \"author\": meta[\"Author\"],\n            \"author_email\": meta[\"Author-email\"],\n            \"maintainer\": meta[\"Maintainer\"],\n            \"maintainer_email\": meta[\"Maintainer-email\"],\n            \"license\": meta[\"License\"],\n            \"description\": meta.get_payload(),\n            \"keywords\": meta[\"Keywords\"],\n            \"platform\": meta.get_all(\"Platform\") or (),\n            \"classifiers\": meta.get_all(\"Classifier\") or [],\n            \"download_url\": meta[\"Download-URL\"],\n            \"supported_platform\": meta.get_all(\"Supported-Platform\") or (),\n            \"comment\": self.comment,\n            # Metadata 1.2\n            \"project_urls\": meta.get_all(\"Project-URL\") or (),\n            \"provides_dist\": meta.get_all(\"Provides-Dist\") or (),\n            \"obsoletes_dist\": meta.get_all(\"Obsoletes-Dist\") or (),\n            \"requires_dist\": meta.get_all(\"Requires-Dist\") or (),\n            \"requires_external\": meta.get_all(\"Requires-External\") or (),\n            \"requires_python\": meta.get_all(\"Requires-Python\") or (),\n            # Metadata 2.1\n            \"provides_extras\": meta.get_all(\"Provides-Extra\") or (),\n            \"description_content_type\": meta.get(\"Description-Content-Type\"),\n            # Metadata 2.2\n            \"dynamic\": meta.get_all(\"Dynamic\") or (),\n            # Hashes\n            **self.get_hashes(),\n        }\n        if self.gpg_signature is not None:\n            data[\"gpg_signature\"] = self.gpg_signature\n        return data\n"
  },
  {
    "path": "src/pdm/cli/commands/publish/repository.py",
    "content": "from __future__ import annotations\n\nimport os\nfrom typing import TYPE_CHECKING, Any, Iterable, cast\nfrom urllib.parse import urlparse, urlunparse\n\nimport httpx\nfrom id import AmbientCredentialError, detect_credential\nfrom rich.progress import (\n    BarColumn,\n    DownloadColumn,\n    TimeRemainingColumn,\n    TransferSpeedColumn,\n)\n\nfrom pdm import termui\nfrom pdm.cli.commands.publish.package import PackageFile\nfrom pdm.exceptions import PdmUsageError\nfrom pdm.project import Project\nfrom pdm.project.config import DEFAULT_REPOSITORIES\n\nif TYPE_CHECKING:\n    from typing import Callable, Self\n\n    from httpx._multipart import MultipartStream\n\n    from pdm._types import RepositoryConfig\n\n\nclass CallbackWrapperStream(httpx.SyncByteStream):\n    def __init__(self, stream: httpx.SyncByteStream, callback: Callable[[Self], Any]) -> None:\n        self._stream = stream\n        self._callback = callback\n        self.bytes_read = 0\n\n    def __iter__(self) -> Iterable[bytes]:\n        for chunk in self._stream:\n            self.bytes_read += len(chunk)\n            self._callback(self)\n            yield chunk\n\n\nclass Repository:\n    def __init__(self, project: Project, config: RepositoryConfig) -> None:\n        self.url = cast(str, config.url)\n        self.session = project.environment._build_session([config])\n\n        self._credentials_to_save: tuple[str, str, str] | None = None\n        self.ui = project.core.ui\n        username, password = self._ensure_credentials(config.username, config.password)\n        self.session.auth = (username, password)\n\n    def _ensure_credentials(self, username: str | None, password: str | None) -> tuple[str, str]:\n        from pdm.models.auth import keyring\n\n        parsed_url = urlparse(self.url)\n        netloc = parsed_url.netloc\n        if username and password:\n            return username, password\n        if password:\n            return \"__token__\", password\n        if parsed_url.username is not None and parsed_url.password is not None:\n            return parsed_url.username, parsed_url.password\n        if keyring.enabled:\n            auth = keyring.get_auth_info(self.url, username)\n            if auth is not None:\n                return auth\n        token = self._get_pypi_token_via_oidc()\n        if token is not None:\n            return \"__token__\", token\n        if not termui.is_interactive():\n            raise PdmUsageError(\"Username and password are required\")\n        username, password, save = self._prompt_for_credentials(netloc, username)\n        if save and keyring.enabled and termui.confirm(\"Save credentials to keyring?\"):\n            self._credentials_to_save = (netloc, username, password)\n        return username, password\n\n    def _get_pypi_token_via_oidc(self) -> str | None:\n        self.ui.echo(\"Getting PyPI token via OIDC...\")\n\n        try:\n            parsed_url = urlparse(self.url)\n            audience_url = urlunparse(parsed_url._replace(path=\"/_/oidc/audience\"))\n            resp = self.session.get(audience_url)\n            resp.raise_for_status()\n            audience = cast(str, resp.json()[\"audience\"])\n            oidc_token = detect_credential(audience)\n            if oidc_token is None:\n                self.ui.echo(\n                    \"This platform is not supported for trusted publishing via OIDC\",\n                    err=True,\n                )\n                return None\n            mint_token_url = urlunparse(parsed_url._replace(path=\"/_/oidc/mint-token\"))\n            resp = self.session.post(mint_token_url, json={\"token\": oidc_token})\n            resp.raise_for_status()\n            token = resp.json()[\"token\"]\n        except AmbientCredentialError as e:\n            self.ui.echo(f\"Unable to detect OIDC token for CI platform: {e}\", err=True)\n            return None\n        except httpx.HTTPError:\n            self.ui.echo(\"Failed to get PyPI token via OIDC\", err=True)\n            return None\n        else:\n            if os.getenv(\"GITHUB_ACTIONS\"):\n                # tell GitHub Actions to mask the token in any console logs\n                print(f\"::add-mask::{token}\")\n            return token\n\n    def _prompt_for_credentials(self, service: str, username: str | None) -> tuple[str, str, bool]:\n        from pdm.models.auth import keyring\n\n        if keyring.enabled:\n            cred = keyring.get_auth_info(service, username)\n            if cred is not None:\n                return cred[0], cred[1], False\n        if username is None:\n            username = termui.ask(\"[primary]Username\")\n        password = termui.ask(\"[primary]Password\", password=True)\n        return username, password, True\n\n    def _save_credentials(self, service: str, username: str, password: str) -> None:\n        from pdm.models.auth import keyring\n\n        self.ui.echo(\"Saving credentials to keyring\")\n        keyring.save_auth_info(service, username, password)\n\n    def get_release_urls(self, packages: list[PackageFile]) -> Iterable[str]:\n        if self.url.startswith(DEFAULT_REPOSITORIES[\"pypi\"].rstrip(\"/\")):\n            base = \"https://pypi.org/\"\n        elif self.url.startswith(DEFAULT_REPOSITORIES[\"testpypi\"].rstrip(\"/\")):\n            base = \"https://test.pypi.org/\"\n        else:\n            return set()\n        return {f\"{base}project/{package.metadata['name']}/{package.metadata['version']}/\" for package in packages}\n\n    def upload(self, package: PackageFile) -> httpx.Response:\n        data_fields = package.metadata_dict\n        data_fields.update(\n            {\n                \":action\": \"file_upload\",\n                \"protocol_version\": \"1\",\n            }\n        )\n        with self.ui.make_progress(\n            \" [progress.percentage]{task.percentage:>3.0f}%\",\n            BarColumn(),\n            DownloadColumn(),\n            \"•\",\n            TimeRemainingColumn(\n                compact=True,\n                elapsed_when_finished=True,\n            ),\n            \"•\",\n            TransferSpeedColumn(),\n        ) as progress:\n            progress.console.print(f\"Uploading [success]{package.base_filename}\")\n\n            with open(package.filename, \"rb\") as fp:\n                file_fields = [(\"content\", (package.base_filename, fp, \"application/octet-stream\"))]\n\n                def on_upload(monitor: CallbackWrapperStream) -> None:\n                    progress.update(job, completed=monitor.bytes_read)\n\n                request = self.session.build_request(\"POST\", self.url, data=data_fields, files=file_fields)\n                stream = cast(\"MultipartStream\", request.stream)\n                request.stream = CallbackWrapperStream(stream, on_upload)\n\n                job = progress.add_task(\"\", total=stream.get_content_length())\n                resp = self.session.send(request, follow_redirects=False)\n                if not resp.is_error and self._credentials_to_save is not None:\n                    self._save_credentials(*self._credentials_to_save)\n                    self._credentials_to_save = None\n                return resp\n"
  },
  {
    "path": "src/pdm/cli/commands/python.py",
    "content": "from __future__ import annotations\n\nimport os\nimport shutil\nimport sys\nimport tempfile\nfrom argparse import ArgumentParser\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, cast\n\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.options import verbose_option\nfrom pdm.environments import BareEnvironment\nfrom pdm.exceptions import InstallationError, PdmArgumentError\nfrom pdm.models.python import PythonInfo\nfrom pdm.termui import Verbosity\nfrom pdm.utils import get_all_installable_python_versions\n\nif TYPE_CHECKING:\n    from argparse import ArgumentParser, Namespace, _SubParsersAction\n    from typing import Any\n\n    from pdm.project.core import Project\n\n\nclass Command(BaseCommand):\n    \"\"\"Manage installed Python interpreters\"\"\"\n\n    arguments = ()\n\n    def add_arguments(self, parser: ArgumentParser) -> None:\n        self.parser = parser\n        subparsers = parser.add_subparsers(title=\"commands\", metavar=\"\")\n        ListCommand.register_to(subparsers, name=\"list\")\n        RemoveCommand.register_to(subparsers, name=\"remove\")\n        InstallCommand.register_to(subparsers, name=\"install\")\n        LinkCommand.register_to(subparsers, name=\"link\")\n        FindCommand.register_to(subparsers, name=\"find\")\n\n    @classmethod\n    def register_to(cls, subparsers: _SubParsersAction, name: str | None = None, **kwargs: Any) -> None:\n        return super().register_to(subparsers, name, aliases=[\"py\"], **kwargs)\n\n    def handle(self, project: Project, options: Namespace) -> None:\n        self.parser.print_help()\n\n\nclass ListCommand(BaseCommand):\n    \"\"\"List all Python interpreters installed with PDM\"\"\"\n\n    arguments = (verbose_option,)\n\n    def handle(self, project: Project, options: Namespace) -> None:\n        from findpython.providers.rye import RyeProvider\n\n        ui = project.core.ui\n        provider = RyeProvider(root=Path(project.config[\"python.install_root\"]).expanduser())\n        for version in provider.find_pythons():\n            ui.echo(f\"[success]{version}[/] ({version.executable})\")\n\n\nclass RemoveCommand(BaseCommand):\n    \"\"\"Remove a Python interpreter installed with PDM\"\"\"\n\n    arguments = (verbose_option,)\n\n    def add_arguments(self, parser: ArgumentParser) -> None:\n        parser.add_argument(\"version\", help=\"The Python version to remove. E.g. cpython@3.10.3\")\n\n    def handle(self, project: Project, options: Namespace) -> None:\n        ui = project.core.ui\n        root = Path(project.config[\"python.install_root\"]).expanduser()\n        if not root.exists():\n            ui.error(f\"No Python interpreter found for {options.version!r}\")\n            sys.exit(1)\n        version = str(options.version)\n        if root.joinpath(version).exists():\n            version_dir = root.joinpath(version)\n        else:\n            version = options.version.lower()\n            if \"@\" not in version:  # pragma: no cover\n                version = f\"cpython@{version}\"\n            version_dir = root.joinpath(version)\n            if not version_dir.exists():\n                ui.error(f\"No Python interpreter found for {options.version!r}\")\n                ui.echo(\"Installed Pythons:\", err=True)\n                for child in root.iterdir():\n                    if not child.name.startswith(\".\"):\n                        ui.echo(f\"  {child.name}\", err=True)\n                sys.exit(1)\n        if version_dir.is_symlink():\n            version_dir.unlink()\n        else:\n            shutil.rmtree(version_dir, ignore_errors=True)\n        ui.echo(f\"[success]Removed installed[/] {options.version}\", verbosity=Verbosity.NORMAL)\n\n\nclass InstallCommand(BaseCommand):\n    \"\"\"Install a Python interpreter with PDM\"\"\"\n\n    arguments = (verbose_option,)\n\n    def add_arguments(self, parser: ArgumentParser) -> None:\n        parser.add_argument(\n            \"version\",\n            help=\"The Python version to install (e.g. cpython@3.10.3). If left empty, \"\n            \"highest cPython version that matches this platform/arch is installed. \"\n            \"If pyproject.toml with requires-python is available, this is considered as well.\",\n            nargs=\"?\",\n        )\n        parser.add_argument(\"--list\", \"-l\", action=\"store_true\", help=\"List all available Python versions\")\n        parser.add_argument(\n            \"--min\",\n            action=\"store_true\",\n            help=\"Use minimum instead of highest version for installation if `version` is left empty\",\n        )\n\n    def handle(self, project: Project, options: Namespace) -> None:\n        if options.list:\n            for version in get_all_installable_python_versions(build_dir=False):\n                project.core.ui.echo(str(version))\n            return\n        version = options.version\n        if version is None:\n            match = project.get_best_matching_cpython_version(options.min)\n            if match is not None:\n                version = str(match)\n\n        if version is None:\n            raise PdmArgumentError(\"Please specify a Python version to be installed. E.g. cpython@3.10.3\")\n\n        self.install_python(project, version)\n\n    @staticmethod\n    def install_python(project: Project, request: str) -> PythonInfo:\n        from pbs_installer import download, get_download_link, install_file\n        from pbs_installer._install import THIS_ARCH\n\n        from pdm.misc import sysconfig_patcher\n        from pdm.termui import logger\n\n        ui = project.core.ui\n        root = Path(project.config[\"python.install_root\"]).expanduser()\n\n        implementation, _, version = request.rpartition(\"@\")\n        implementation = implementation.lower() or \"cpython\"\n        version, _, arch = version.partition(\"-\")\n        arch = \"x86\" if arch == \"32\" else (arch or THIS_ARCH)\n\n        ver, python_file = get_download_link(version, implementation=implementation, arch=arch, build_dir=False)\n        ver_str = str(ver)\n        spinner_msg = f\"Downloading [success]{ver_str}[/]\"\n        if ui.verbosity >= Verbosity.DETAIL:\n            download_url = python_file[0] if isinstance(python_file, (tuple, list)) else python_file\n            spinner_msg += f\" {download_url}\"\n\n        with ui.open_spinner(spinner_msg) as spinner:\n            destination = root / ver_str\n            logger.debug(\"Installing %s to %s\", ver_str, destination)\n            env = BareEnvironment(project)\n            install_root = destination\n            if install_root.joinpath(\"install\").exists():\n                install_root = install_root.joinpath(\"install\")\n            interpreter = install_root / \"bin\" / \"python3\" if sys.platform != \"win32\" else destination / \"python.exe\"\n            if not destination.exists() or not interpreter.exists():\n                shutil.rmtree(destination, ignore_errors=True)\n                destination.mkdir(parents=True, exist_ok=True)\n                with tempfile.NamedTemporaryFile() as tf:\n                    tf.close()\n                    original_filename = download(python_file, tf.name, env.session)\n                    spinner.update(f\"Installing [success]{ver_str}[/]\")\n                    try:\n                        install_file(tf.name, destination, original_filename)\n                    except ModuleNotFoundError as e:\n                        if \"zstandard is required\" in str(e):\n                            raise InstallationError(\n                                \"zstandard is required to install this Python version. \"\n                                \"Please install it with `pdm self add zstandard`.\"\n                            ) from None\n        if destination.joinpath(\"install\").exists():\n            install_root = destination.joinpath(\"install\")\n            interpreter = install_root / \"bin\" / \"python3\" if sys.platform != \"win32\" else install_root / \"python.exe\"\n        if not interpreter.exists():\n            raise InstallationError(\"Installation failed, please try again.\")\n        # Patch sysconfig and pkgconfig files with correct prefixes\n        sysconfig_patcher.patch(install_root)\n        python_info = PythonInfo.from_path(interpreter)\n        ui.echo(\n            f\"[success]Successfully installed[/] {python_info.implementation}@{python_info.version}\",\n            verbosity=Verbosity.NORMAL,\n        )\n        ui.echo(f\"[info]Version:[/] {python_info.version}\", verbosity=Verbosity.NORMAL)\n        ui.echo(f\"[info]Executable:[/] {python_info.path}\", verbosity=Verbosity.NORMAL)\n        return python_info\n\n\nclass LinkCommand(BaseCommand):\n    \"\"\"Link an external Python interpreter to PDM\"\"\"\n\n    arguments = (verbose_option,)\n\n    def add_arguments(self, parser: ArgumentParser) -> None:\n        parser.add_argument(\"interpreter\", help=\"The path to the Python interpreter to link\")\n        parser.add_argument(\"--name\", help=\"The name of the link\")\n\n    def handle(self, project: Project, options: Namespace) -> None:\n        python_info = PythonInfo.from_path(options.interpreter)\n        if not python_info.valid:\n            raise PdmArgumentError(\"Invalid Python interpreter\")\n        if options.name is None:\n            link_name = f\"{python_info.implementation}@{python_info.identifier}\"\n        else:\n            link_name = cast(str, options.name)\n        link_path = Path(project.config[\"python.install_root\"]).expanduser() / link_name\n        if link_path.exists():\n            raise PdmArgumentError(f\"Link {link_name} already exists\")\n        exe_dir = python_info.path.parent\n        if exe_dir.name in (\"Scripts\", \"bin\"):\n            exe_dir = exe_dir.parent\n        link_path.parent.mkdir(parents=True, exist_ok=True)\n        link_path.symlink_to(exe_dir)\n        project.core.ui.echo(f\"[success]Successfully linked {link_name} to {exe_dir}[/]\")\n\n\nclass FindCommand(BaseCommand):\n    \"\"\"Search for a Python interpreter\"\"\"\n\n    arguments = (verbose_option,)\n\n    def add_arguments(self, parser: ArgumentParser) -> None:\n        parser.add_argument(\"--managed\", action=\"store_true\", help=\"Only find interpreters managed by PDM\")\n        parser.add_argument(\"request\", help=\"The Python version to find. E.g. 3.12, cpython@3.13\")\n\n    def handle(self, project: Project, options: Namespace) -> None:\n        from findpython import Finder\n\n        if options.managed:\n            old_rye_root = os.getenv(\"RYE_PY_ROOT\")\n            os.environ[\"RYE_PY_ROOT\"] = os.path.expanduser(project.config[\"python.install_root\"])\n            try:\n                finder = Finder(resolve_symlinks=True, selected_providers=[\"rye\"])\n            finally:\n                if old_rye_root:  # pragma: no cover\n                    os.environ[\"RYE_PY_ROOT\"] = old_rye_root\n                else:\n                    del os.environ[\"RYE_PY_ROOT\"]\n        else:\n            finder = project._get_python_finder()\n\n        python_version = finder.find(options.request)\n        location = (\n            \"managed installations\"\n            if options.managed\n            else \"virtual environments, managed installations, or search paths\"\n        )\n        if python_version is None:\n            project.core.ui.error(f\"Python interpreter {options.request!r} not found in {location}\")\n            raise SystemExit(1)\n        print(python_version.executable)\n"
  },
  {
    "path": "src/pdm/cli/commands/remove.py",
    "content": "from __future__ import annotations\n\nimport argparse\nfrom typing import TYPE_CHECKING\n\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.filters import GroupSelection\nfrom pdm.cli.hooks import HookManager\nfrom pdm.cli.options import (\n    dry_run_option,\n    frozen_lockfile_option,\n    install_group,\n    lockfile_option,\n    override_option,\n    skip_option,\n    venv_option,\n)\nfrom pdm.exceptions import PdmUsageError, ProjectError\nfrom pdm.utils import normalize_name\n\nif TYPE_CHECKING:\n    from typing import Collection\n\n    from pdm.project import Project\n\n\nclass Command(BaseCommand):\n    \"\"\"Remove packages from pyproject.toml\"\"\"\n\n    arguments = (\n        *BaseCommand.arguments,\n        install_group,\n        dry_run_option,\n        lockfile_option,\n        override_option,\n        frozen_lockfile_option,\n        skip_option,\n        venv_option,\n    )\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\n            \"-d\",\n            \"--dev\",\n            default=False,\n            action=\"store_true\",\n            help=\"Remove packages from dev dependencies\",\n        )\n        parser.add_argument(\"-G\", \"--group\", help=\"Specify the target dependency group to remove from\")\n        parser.add_argument(\n            \"--no-sync\",\n            dest=\"sync\",\n            default=True,\n            action=\"store_false\",\n            help=\"Only write pyproject.toml and do not uninstall packages\",\n        )\n        parser.add_argument(\"packages\", nargs=\"+\", help=\"Specify the packages to remove\")\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        self.do_remove(\n            project,\n            selection=GroupSelection.from_options(project, options),\n            sync=options.sync,\n            packages=options.packages,\n            no_editable=options.no_editable,\n            no_self=options.no_self,\n            dry_run=options.dry_run,\n            fail_fast=options.fail_fast,\n            hooks=HookManager(project, options.skip),\n        )\n\n    @staticmethod\n    def do_remove(\n        project: Project,\n        selection: GroupSelection,\n        sync: bool = True,\n        packages: Collection[str] = (),\n        no_editable: bool = False,\n        no_self: bool = False,\n        dry_run: bool = False,\n        fail_fast: bool = False,\n        hooks: HookManager | None = None,\n    ) -> None:\n        \"\"\"Remove packages from working set and pyproject.toml\"\"\"\n\n        from pdm.cli.actions import do_lock, do_sync\n        from pdm.cli.utils import check_project_file\n        from pdm.models.requirements import parse_requirement\n        from pdm.utils import cd\n\n        hooks = hooks or HookManager(project)\n        check_project_file(project)\n        if not packages:\n            raise PdmUsageError(\"Must specify at least one package to remove.\")\n        group = selection.one()\n        lock_groups = project.lockfile.groups\n        project.pyproject.open_for_write()\n        deps, setter = project.use_pyproject_dependencies(group, selection.dev or False)\n        project.core.ui.echo(\n            f\"Removing {'[bold]global[/] ' if project.is_global else ''}packages from [primary]{group}[/] \"\n            f\"{'dev-' if selection.dev else ''}dependencies: \" + \", \".join(f\"[req]{name}[/]\" for name in packages)\n        )\n        tracked_names: set[str] = set()\n        with cd(project.root):\n            for name in packages:\n                req = parse_requirement(name)\n                matched_indexes = sorted((i for i, r in enumerate(deps) if req.matches(r)), reverse=True)\n                if not matched_indexes:\n                    raise ProjectError(f\"[req]{name}[/] does not exist in [primary]{group}[/] dependencies.\")\n                for i in matched_indexes:\n                    del deps[i]\n                tracked_names.add(normalize_name(name))\n        setter(deps)\n\n        if not dry_run:\n            project.pyproject.write()\n        if lock_groups and group not in lock_groups:\n            project.core.ui.warn(f\"Group [success]{group}[/] isn't in lockfile, skipping lock.\")\n            return\n        # It may remove the whole group, exclude it from lock groups first\n        project_groups = project.iter_groups()\n        if lock_groups is not None:\n            lock_groups = [g for g in lock_groups if g in project_groups]\n        do_lock(project, \"reuse\", dry_run=dry_run, tracked_names=tracked_names, hooks=hooks, groups=lock_groups)\n        if sync:\n            do_sync(\n                project,\n                selection=GroupSelection(project, default=False, groups=[group], exclude_non_existing=True),\n                clean=True,\n                no_editable=no_editable,\n                no_self=no_self,\n                dry_run=dry_run,\n                fail_fast=fail_fast,\n                hooks=hooks,\n            )\n"
  },
  {
    "path": "src/pdm/cli/commands/run.py",
    "content": "from __future__ import annotations\n\nimport argparse\nimport contextlib\nimport itertools\nimport os\nimport re\nimport shlex\nimport signal\nimport subprocess\nimport sys\nfrom functools import partial\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Mapping, NamedTuple, Sequence, cast\n\nfrom rich import print_json\n\nfrom pdm import termui\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.hooks import HookManager\nfrom pdm.cli.options import skip_option, venv_option\nfrom pdm.cli.utils import check_project_file\nfrom pdm.exceptions import PdmUsageError\nfrom pdm.signals import pdm_signals\nfrom pdm.utils import deprecation_warning, expand_env_vars, is_path_relative_to\n\nif TYPE_CHECKING:\n    from types import FrameType\n    from typing import Any, Iterator, TypedDict\n\n    from pdm.environments import BaseEnvironment\n    from pdm.project import Project\n\n    class EnvFileOptions(TypedDict, total=True):\n        override: str\n\n    class TaskOptions(TypedDict, total=False):\n        env: Mapping[str, str]\n        env_file: EnvFileOptions | str | None\n        help: str\n        keep_going: bool\n        site_packages: bool\n        working_dir: str\n\n\ndef merge_options(*options: TaskOptions | None) -> TaskOptions:\n    \"\"\"Merge multiple options dicts. For the same key, the last one wins.\"\"\"\n    return cast(\n        \"TaskOptions\",\n        {\n            \"env\": {k: v for opts in options if opts for k, v in opts.get(\"env\", {}).items()},\n            **{k: v for opts in options if opts for k, v in opts.items() if k not in (\"env\", \"help\", \"keep_going\")},\n        },\n    )\n\n\nexec_opts = merge_options  # Alias for merge_options\n\n\nRE_ARGS_PLACEHOLDER = re.compile(r\"\\{args(?::(?P<default>[^}]*))?\\}\")\nRE_PDM_PLACEHOLDER = re.compile(r\"\\{pdm\\}\")\n\n\ndef _interpolate_args(script: str, args: Sequence[str]) -> tuple[str, bool]:\n    \"\"\"Interpolate the `{args:[defaults]} placeholder in a string\"\"\"\n    import shlex\n\n    def replace(m: re.Match[str]) -> str:\n        default = m.group(\"default\") or \"\"\n        return shlex.join(args) if args else default\n\n    interpolated, count = RE_ARGS_PLACEHOLDER.subn(replace, script)\n    return interpolated, count > 0\n\n\ndef _interpolate_pdm(script: str) -> str:\n    \"\"\"Interpolate the `{pdm} placeholder in a string\"\"\"\n    executable_path = Path(sys.executable)\n    pdm_executable = shlex.join([executable_path.as_posix(), \"-m\", \"pdm\"])\n\n    interpolated = RE_PDM_PLACEHOLDER.sub(pdm_executable, script)\n    return interpolated\n\n\ndef interpolate(script: str, args: Sequence[str]) -> tuple[str, bool]:\n    \"\"\"Interpolate the `{args:[defaults]} placeholder in a string\"\"\"\n\n    script, args_interpolated = _interpolate_args(script, args)\n    script = _interpolate_pdm(script)\n    return script, args_interpolated\n\n\n_METADATA_REGEX = r\"(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\\s(?P<content>(^#(| .*)$\\s)+)^# ///$\"\n\n\ndef read_script_metadata(script: str, section: str) -> dict[str, Any] | None:\n    # Adapted from https://packaging.python.org/en/latest/specifications/inline-script-metadata/\n    if sys.version_info >= (3, 11):\n        import tomllib\n    else:\n        import tomli as tomllib\n\n    matches = [m for m in re.finditer(_METADATA_REGEX, script) if m.group(\"type\") == section]\n    if len(matches) > 1:\n        raise ValueError(f\"Multiple {section} blocks found\")\n    elif len(matches) == 1:\n        content = \"\".join(\n            line[2:] if line.startswith(\"# \") else line[1:]\n            for line in matches[0].group(\"content\").splitlines(keepends=True)\n        )\n        return tomllib.loads(content)\n    else:\n        return None\n\n\nclass Task(NamedTuple):\n    kind: str\n    name: str\n    args: str | Sequence[str]\n    options: TaskOptions\n\n    def __str__(self) -> str:\n        return f\"<task [primary]{self.name}[/]>\"\n\n    @property\n    def short_description(self) -> str:\n        \"\"\"\n        A short one line task description\n        \"\"\"\n        if self.kind == \"composite\":\n            fallback = f\" {termui.Emoji.ARROW_SEPARATOR} \".join(self.args)\n        else:\n            lines = [line.strip() for line in str(self.args).splitlines() if line.strip()]\n            fallback = f\"{lines[0]}{termui.Emoji.ELLIPSIS}\" if len(lines) > 1 else lines[0]\n        return self.options.get(\"help\", fallback)\n\n\nclass TaskRunner:\n    \"\"\"The task runner for pdm project\"\"\"\n\n    TYPES = (\"cmd\", \"shell\", \"call\", \"composite\")\n    OPTIONS = (\"env\", \"env_file\", \"help\", \"keep_going\", \"working_dir\", \"site_packages\")\n\n    def __init__(self, project: Project, hooks: HookManager) -> None:\n        self.project = project\n        global_options = cast(\n            \"TaskOptions\",\n            self.project.scripts.get(\"_\", {}) if self.project.scripts else {},\n        )\n        self.global_options = global_options.copy()\n        self.recreate_env = False\n        self.hooks = hooks\n\n    def _get_script_env(self, script_file: str) -> BaseEnvironment:\n        import hashlib\n\n        from pdm.cli.commands.venv.backends import BACKENDS\n        from pdm.environments import PythonEnvironment\n        from pdm.installers.core import install_requirements\n        from pdm.models.venv import get_venv_python\n\n        with open(script_file, encoding=\"utf8\") as f:\n            metadata = read_script_metadata(f.read(), \"script\")\n        if not metadata:\n            return self.project.environment\n        tool_config = metadata.pop(\"tool\", {})\n        script_project = self.project.core.create_project()\n        script_project.pyproject.set_data(\n            {\"project\": {\"name\": \"temp-project\", **metadata, \"version\": \"0.0.0\"}, \"tool\": tool_config}\n        )\n        venv_name = hashlib.md5(os.path.realpath(script_file).encode(\"utf-8\"), usedforsecurity=False).hexdigest()\n        venv_backend = BACKENDS[script_project.config[\"venv.backend\"]](script_project, None)\n        venv = venv_backend.get_location(None, venv_name)\n        with contextlib.ExitStack() as stack:\n            if venv.exists() and not self.recreate_env:\n                stack.enter_context(self.project.core.ui.open_spinner(\"[info]Reusing existing script environment\"))\n            else:\n                stack.enter_context(self.project.core.ui.open_spinner(\"[info]Creating environment for script\"))\n                venv = venv_backend.create(venv_name=venv_name, force=True)\n            self.project.core.ui.info(f\"Script environment location: {venv}\", verbosity=termui.Verbosity.DETAIL)\n            env = PythonEnvironment(script_project, python=get_venv_python(venv).as_posix())\n            script_project._python = env.interpreter\n            env.project = script_project  # keep a strong reference to the project\n            if reqs := script_project.get_dependencies():\n                install_requirements(reqs, env, clean=True)\n        return env\n\n    def get_task(self, script_name: str) -> Task | None:\n        \"\"\"Get the task with the given name. Return None if not found.\"\"\"\n        if script_name not in self.project.scripts:\n            return None\n        script = cast(\"str | Sequence[str] | Mapping[str,Any]\", self.project.scripts[script_name])\n        if not isinstance(script, Mapping):\n            # Regard as the same as {cmd = ... }\n            kind = \"cmd\"\n            value = script\n            options = {}\n        else:\n            script = dict(script)  # to remove the effect of tomlkit's container type.\n            for key in self.TYPES:\n                if key in script:\n                    kind = key\n                    value = cast(\"str | Sequence[str]\", script.pop(key))\n                    break\n            else:\n                raise PdmUsageError(f\"Script type must be one of ({', '.join(self.TYPES)})\")\n            options = script.copy()\n        unknown_options = set(options) - set(self.OPTIONS)\n        if unknown_options:\n            raise PdmUsageError(f\"Unknown options for task {script_name}: {', '.join(unknown_options)}\")\n        return Task(kind, script_name, value, cast(\"TaskOptions\", options))\n\n    def expand_command(self, env: BaseEnvironment, command: str) -> str:\n        expanded_command = os.path.expanduser(command)\n        if expanded_command.replace(os.sep, \"/\").startswith((\"./\", \"../\")):\n            abspath = os.path.abspath(expanded_command)\n            if not os.path.isfile(abspath):\n                raise PdmUsageError(f\"Command [success]'{command}'[/] is not a valid executable.\")\n            return abspath\n        result = env.which(command)\n        if not result:\n            raise PdmUsageError(f\"Command [success]'{command}'[/] is not found in your PATH.\")\n        return result\n\n    def _run_process(\n        self,\n        args: Sequence[str] | str,\n        chdir: bool = False,\n        shell: bool = False,\n        site_packages: bool = False,\n        env: Mapping[str, str] | None = None,\n        env_file: EnvFileOptions | str | None = None,\n        working_dir: str | None = None,\n    ) -> int:\n        \"\"\"Run command in a subprocess and return the exit code.\"\"\"\n        import dotenv\n        from dotenv.main import resolve_variables\n\n        project = self.project\n        if not shell and args[0].endswith(\".py\"):\n            project_env = self._get_script_env(os.path.expanduser(args[0]))\n        else:\n            check_project_file(project)\n            project_env = project.environment\n        this_path = project_env.get_paths()[\"scripts\"]\n        os.environ.update(project_env.process_env)\n        if env_file is not None:\n            if isinstance(env_file, str):\n                path = env_file\n                override = False\n            else:\n                path = env_file[\"override\"]\n                override = True\n\n            project.core.ui.echo(\n                f\"Loading .env file: [success]{env_file}[/]\",\n                err=True,\n                verbosity=termui.Verbosity.DETAIL,\n            )\n            dotenv.load_dotenv(self.project.root / path, override=override)\n        if env:\n            os.environ.update(resolve_variables(env.items(), override=True))\n        if shell:\n            assert isinstance(args, str)\n            # environment variables will be expanded by shell\n            process_cmd: str | Sequence[str] = args\n        else:\n            assert isinstance(args, Sequence)\n            command, *args = (expand_env_vars(arg) for arg in args)\n            if command.endswith(\".py\"):\n                args = [command, *args]\n                command = str(project_env.interpreter.executable)\n            expanded_command = self.expand_command(project_env, command)\n            real_command = os.path.realpath(expanded_command)\n            process_cmd = [expanded_command, *args]\n            if (\n                project_env.is_local\n                and not site_packages\n                and (\n                    os.path.basename(real_command).startswith(\"python\")\n                    or is_path_relative_to(expanded_command, this_path)\n                )\n            ):\n                # The executable belongs to the local packages directory.\n                # Don't load system site-packages\n                os.environ[\"NO_SITE_PACKAGES\"] = \"1\"\n\n        cwd = (project.root / working_dir) if working_dir else project.root if chdir else None\n\n        def forward_signal(signum: int, frame: FrameType | None) -> None:\n            if sys.platform == \"win32\" and signum == signal.SIGINT:\n                signum = signal.SIGTERM\n            process.send_signal(signum)\n\n        process_env = os.environ.copy()\n        process_env.update({\"PDM_RUN_CWD\": str(Path.cwd())})\n        handle_term = signal.signal(signal.SIGTERM, forward_signal)\n        handle_int = signal.signal(signal.SIGINT, forward_signal)\n        process = subprocess.Popen(process_cmd, cwd=cwd, shell=shell, bufsize=0, close_fds=False, env=process_env)\n        retcode = process.wait()\n        signal.signal(signal.SIGTERM, handle_term)\n        signal.signal(signal.SIGINT, handle_int)\n        return retcode\n\n    def run_task(\n        self, task: Task, args: Sequence[str] = (), opts: TaskOptions | None = None, seen: set[str] | None = None\n    ) -> int:\n        \"\"\"Run the named task with the given arguments.\n\n        Args:\n            task: The task to run\n            args: The extra arguments passed to the task\n            opts: The options passed from parent if any\n            seen: The set of seen tasks to prevent recursive calls\n        \"\"\"\n        kind, _, value, options = task\n        shell = False\n        if kind == \"cmd\":\n            if isinstance(value, str):\n                cmd, interpolated = interpolate(value, args)\n                value = shlex.split(cmd)\n            else:\n                agg = [interpolate(part, args) for part in value]\n                interpolated = any(row[1] for row in agg)\n                # In case of multiple default, we need to split the resulting string.\n                parts: Iterator[list[str]] = (\n                    shlex.split(part) if interpolated else [part] for part, interpolated in agg\n                )\n                # We flatten the nested list to obtain a list of arguments\n                value = list(itertools.chain(*parts))\n            args = value if interpolated else [*value, *args]\n        elif kind == \"shell\":\n            assert isinstance(value, str)\n            script, interpolated = interpolate(value, args)\n            args = script if interpolated else \" \".join([script, *args])\n            shell = True\n        elif kind == \"call\":\n            assert isinstance(value, str)\n            module, _, func = value.partition(\":\")\n            if not module or not func:\n                raise PdmUsageError(\"Python callable must be in the form <module_name>:<callable_name>\")\n            short_name = \"_1\"\n            if re.search(r\"\\(.*?\\)\", func) is None:\n                func += \"()\"\n            args = [\"python\", \"-c\", f\"import sys, {module} as {short_name};sys.exit({short_name}.{func})\", *list(args)]\n        elif kind == \"composite\":\n            assert isinstance(value, list)\n\n        self.display_task(task, args)\n\n        if kind == \"composite\":\n            args = list(args)\n            should_interpolate = any(RE_ARGS_PLACEHOLDER.search(script) for script in value)\n            should_interpolate = should_interpolate or any(RE_PDM_PLACEHOLDER.search(script) for script in value)\n            composite_code = 0\n            keep_going = options.pop(\"keep_going\", False) if options else False\n            for script in value:\n                if should_interpolate:\n                    script, _ = interpolate(script, args)\n                split = shlex.split(script)\n                cmd = split[0]\n                subargs = split[1:] + ([] if should_interpolate else args)\n                code = self.run(cmd, subargs, merge_options(options, opts), chdir=True, seen=seen)\n                if code != 0:\n                    if not keep_going:\n                        return code\n                    composite_code = code\n            return composite_code\n        return self._run_process(args, chdir=True, shell=shell, **merge_options(self.global_options, options, opts))  # type: ignore[misc]\n\n    def display_task(self, task: Task, args: Sequence[str]) -> None:\n        \"\"\"Display a task given current verbosity and settings\"\"\"\n        is_verbose = self.project.core.ui.verbosity >= termui.Verbosity.DETAIL\n        if not (is_verbose or self.project.config.get(\"scripts.show_header\")):\n            return\n        args = task.args if task.kind == \"composite\" else args\n        content = args if is_verbose else task.short_description\n        self.project.core.ui.echo(\n            f\"Running {task}: [success]{content}[/]\",\n            err=True,\n            verbosity=termui.Verbosity.NORMAL,\n        )\n\n    def run(\n        self,\n        command: str,\n        args: list[str],\n        opts: TaskOptions | None = None,\n        chdir: bool = False,\n        seen: set[str] | None = None,\n    ) -> int:\n        \"\"\"Run a command or script with the given arguments.\"\"\"\n        if command in self.hooks.skip:\n            return 0\n        if seen is None:\n            seen = set()\n        task = self.get_task(command)\n        if task is not None:\n            if task.kind == \"composite\":\n                if command in seen:\n                    raise PdmUsageError(f\"Script {command} is recursive.\")\n                seen = {command, *seen}\n\n            self.hooks.try_emit(\"pre_script\", script=command, args=args)\n            pre_task = self.get_task(f\"pre_{command}\")\n            if pre_task is not None and self.hooks.should_run(pre_task.name):\n                code = self.run_task(pre_task, opts=opts)\n                if code != 0:\n                    return code\n            code = self.run_task(task, args, opts=opts, seen=seen)\n            if code != 0:\n                return code\n            post_task = self.get_task(f\"post_{command}\")\n            if post_task is not None and self.hooks.should_run(post_task.name):\n                code = self.run_task(post_task, opts=opts)\n            self.hooks.try_emit(\"post_script\", script=command, args=args)\n            return code\n        else:\n            return self._run_process([command, *args], chdir=chdir, **merge_options(self.global_options, opts))  # type: ignore[misc]\n\n    def show_list(self) -> None:\n        if not self.project.scripts:\n            return\n        columns = [\"Name\", \"Type\", \"Description\"]\n        result = []\n        for name in sorted(self.project.scripts):\n            if name.startswith(\"_\"):\n                continue\n            task = self.get_task(name)\n            assert task is not None\n            result.append(\n                (\n                    f\"[success]{name}[/]\",\n                    task.kind,\n                    task.short_description,\n                )\n            )\n        self.project.core.ui.display_columns(result, columns)\n\n    def as_json(self) -> dict[str, Any]:\n        out = {}\n        for name in sorted(self.project.scripts):\n            if name == \"_\":\n                data = out[\"_\"] = dict(name=\"_\", kind=\"shared\", help=\"Shared options\", **self.global_options)\n                _fix_env_file(data)\n                continue\n            task = self.get_task(name)\n            assert task is not None\n            data = out[name] = {\n                \"name\": name,\n                \"kind\": task.kind,\n                \"help\": task.short_description,\n                \"args\": task.args,  # type: ignore[dict-item]\n            }\n            data.update(**task.options)\n            _fix_env_file(data)\n        return out\n\n\ndef _fix_env_file(data: dict[str, Any]) -> dict[str, Any]:\n    env_file = data.get(\"env_file\")\n    if isinstance(env_file, dict):\n        del data[\"env_file\"]\n        data[\"env_file.override\"] = env_file.get(\"override\")\n    return data\n\n\nclass Command(BaseCommand):\n    \"\"\"Run commands or scripts with local packages loaded\"\"\"\n\n    arguments = (*BaseCommand.arguments, skip_option, venv_option)\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        action = parser.add_mutually_exclusive_group()\n        action.add_argument(\n            \"-l\",\n            \"--list\",\n            action=\"store_true\",\n            help=\"Show all available scripts defined in pyproject.toml\",\n        )\n        action.add_argument(\n            \"-j\",\n            \"--json\",\n            action=\"store_true\",\n            help=\"Output all scripts infos in JSON\",\n        )\n        exec = parser.add_argument_group(\"Execution parameters\")\n        exec.add_argument(\n            \"-s\",\n            \"--site-packages\",\n            action=\"store_true\",\n            help=\"Load site-packages from the selected interpreter\",\n        )\n        exec.add_argument(\n            \"--recreate\", action=\"store_true\", help=\"Recreate the script environment for self-contained scripts\"\n        )\n        exec.add_argument(\"script\", nargs=\"?\", help=\"The command to run\")\n        exec.add_argument(\n            \"args\",\n            nargs=argparse.REMAINDER,\n            help=\"Arguments that will be passed to the command\",\n        )\n\n    def get_runner(self, project: Project, hooks: HookManager, options: argparse.Namespace) -> TaskRunner:\n        if (runner_cls := getattr(self, \"runner_cls\", None)) is not None:  # pragma: no cover\n            deprecation_warning(\"runner_cls attribute is deprecated, use get_runner method instead.\")\n            runner = cast(\"type[TaskRunner]\", runner_cls)(project, hooks)\n        else:\n            runner = TaskRunner(project, hooks)\n        runner.recreate_env = options.recreate\n        if options.site_packages:\n            runner.global_options[\"site_packages\"] = True\n        return runner\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        hooks = HookManager(project, options.skip)\n        runner = self.get_runner(project, hooks, options)\n        if options.list:\n            return runner.show_list()\n        if options.json:\n            return print_json(data=runner.as_json())\n        if not options.script:\n            project.core.ui.warn(\"No command is given, default to the Python REPL.\")\n            options.script = \"python\"\n        hooks.try_emit(\"pre_run\", script=options.script, args=options.args)\n        exit_code = runner.run(options.script, options.args)\n        hooks.try_emit(\"post_run\", script=options.script, args=options.args)\n        sys.exit(exit_code)\n\n\ndef run_script_if_present(script_name: str, sender: Project, hooks: HookManager, **kwargs: Any) -> None:\n    \"\"\"A signal handler to run a script if present in the project.\"\"\"\n\n    runner = TaskRunner(sender, hooks)\n    task = runner.get_task(script_name)\n    if task is None:\n        return\n    exit_code = runner.run_task(task)\n    if exit_code != 0:\n        sys.exit(exit_code)\n    # reload project files in case the script modified them\n    sender.project_config.reload()\n    sender.pyproject.reload()\n    sender.lockfile.reload()\n\n\nfor hook in pdm_signals:\n    pdm_signals.signal(hook).connect(partial(run_script_if_present, hook), weak=False)\n"
  },
  {
    "path": "src/pdm/cli/commands/search.py",
    "content": "from __future__ import annotations\n\nimport argparse\nimport textwrap\n\nfrom pdm import termui\nfrom pdm._types import SearchResults\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.options import verbose_option\nfrom pdm.models.working_set import WorkingSet\nfrom pdm.project import Project\nfrom pdm.utils import normalize_name\n\n\ndef print_results(\n    ui: termui.UI,\n    hits: SearchResults,\n    working_set: WorkingSet,\n    terminal_width: int | None = None,\n) -> None:\n    if not hits:\n        return\n    name_column_width = max(len(hit.name) + len(hit.version or \"\") for hit in hits) + 4\n\n    for hit in hits:\n        name = hit.name\n        summary = hit.summary or \"\"\n        if terminal_width is not None:\n            target_width = terminal_width - name_column_width - 5\n            if target_width > 10:\n                # wrap and indent summary to fit terminal\n                summary = (\"\\n\" + \" \" * (name_column_width + 2)).join(textwrap.wrap(summary, target_width))\n        current_width = len(name) + 1\n        spaces = \" \" * (name_column_width - current_width)\n        line = f\"[req]{name}[/]{spaces} - {summary}\"\n        try:\n            ui.echo(line)\n            if normalize_name(name) in working_set:\n                dist = working_set[normalize_name(name)]\n                ui.echo(f\"  INSTALLED: {dist.version}\")\n        except UnicodeEncodeError:\n            pass\n\n\nclass Command(BaseCommand):\n    \"\"\"[DEPRECATED] Search for PyPI packages\"\"\"\n\n    arguments = (verbose_option,)\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\"query\", help=\"Query string to search\")\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        project.core.ui.warn(\n            \"Since pypi.org no longer supports search API, this command is deprecated and will be removed in future versions. \"\n            \"Please visit `https://pypi.org` in the browser to search for packages.\",\n        )\n        return\n"
  },
  {
    "path": "src/pdm/cli/commands/self_cmd.py",
    "content": "from __future__ import annotations\n\nimport argparse\nimport shlex\nimport subprocess\nimport sys\nfrom typing import Any\n\nfrom pdm import termui\nfrom pdm.cli.actions import get_latest_pdm_version_from_pypi\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.options import verbose_option\nfrom pdm.cli.utils import PackageNode, build_dependency_graph\nfrom pdm.compat import Distribution\nfrom pdm.environments import BareEnvironment\nfrom pdm.models.markers import EnvSpec\nfrom pdm.models.working_set import WorkingSet\nfrom pdm.project import Project\nfrom pdm.utils import is_in_zipapp, normalize_name, parse_version\n\nPDM_REPO = \"https://github.com/pdm-project/pdm\"\n\n\ndef list_distributions(plugin_only: bool = False) -> list[Distribution]:\n    result: list[Distribution] = []\n    working_set = WorkingSet()\n    for dist in working_set.values():\n        if not plugin_only or any(ep.group in (\"pdm\", \"pdm.plugin\") for ep in dist.entry_points):\n            result.append(dist)\n    return sorted(result, key=lambda d: d.metadata.get(\"Name\", \"UNKNOWN\"))\n\n\ndef run_pip(project: Project, args: list[str]) -> subprocess.CompletedProcess[str]:\n    if project.config[\"use_uv\"]:\n        if \"--upgrade-strategy\" in args:\n            # uv doesn't support this option\n            args[(i := args.index(\"--upgrade-strategy\")) : i + 2] = []\n        run_args = [*project.core.uv_cmd, \"pip\", *args, \"--python\", sys.executable]\n    else:\n        env = BareEnvironment(project)\n        project.environment = env\n        run_args = [*env.pip_command, *args]\n    project.core.ui.echo(f\"Running pip command: {run_args}\", verbosity=termui.Verbosity.DETAIL)\n\n    result = subprocess.run(\n        run_args,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.STDOUT,\n        check=True,\n        text=True,\n    )\n    project.core.ui.echo(\n        f\"Run pip returns status {result.returncode}: {result.stdout}\", verbosity=termui.Verbosity.DEBUG\n    )\n    return result\n\n\nclass Command(BaseCommand):\n    \"\"\"Manage the PDM program itself (previously known as plugin)\"\"\"\n\n    arguments = (verbose_option,)\n    name = \"self\"\n\n    @classmethod\n    def register_to(\n        cls,\n        subparsers: argparse._SubParsersAction,\n        name: str | None = None,\n        **kwargs: Any,\n    ) -> None:\n        return super().register_to(subparsers, name, aliases=[\"plugin\"], **kwargs)\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        subparsers = parser.add_subparsers(title=\"commands\", metavar=\"\")\n        ListCommand.register_to(subparsers)\n        if not is_in_zipapp():\n            AddCommand.register_to(subparsers)\n            RemoveCommand.register_to(subparsers)\n            UpdateCommand.register_to(subparsers)\n        parser.set_defaults(search_parent=False)\n        self.parser = parser\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        self.parser.print_help()\n\n\nclass ListCommand(BaseCommand):\n    \"\"\"List all packages installed with PDM\"\"\"\n\n    arguments = (verbose_option,)\n    name = \"list\"\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\"--plugins\", action=\"store_true\", help=\"List plugins only\")\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        distributions = list_distributions(plugin_only=options.plugins)\n        echo = project.core.ui.echo\n        if not distributions:\n            # This should not happen when plugin_only is False\n            echo(\"No plugin is installed with PDM\", err=True)\n            sys.exit(1)\n        echo(\"Installed packages:\", err=True)\n        rows = []\n        for dist in distributions:\n            rows.append(\n                (\n                    f\"[success]{dist.metadata.get('Name')}[/]\",\n                    f\"[warning]{dist.metadata.get('Version')}[/]\",\n                    dist.metadata.get(\"Summary\", \"\"),\n                ),\n            )\n        project.core.ui.display_columns(rows)\n\n\nclass AddCommand(BaseCommand):\n    \"\"\"Install packages to the PDM's environment\"\"\"\n\n    arguments = (verbose_option,)\n    name = \"add\"\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\n            \"--pip-args\",\n            help=\"Arguments that will be passed to pip install\",\n            default=\"\",\n        )\n        parser.add_argument(\n            \"packages\",\n            nargs=\"+\",\n            help=\"Specify one or many package names, each package can have a version specifier\",\n        )\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        pip_args = [\"install\", *shlex.split(options.pip_args), *options.packages]\n\n        try:\n            with project.core.ui.open_spinner(f\"Installing packages: {options.packages}\"):\n                run_pip(project, pip_args)\n        except subprocess.CalledProcessError as e:\n            project.core.ui.echo(\"[error]Installation failed:[/]\\n\" + e.output, err=True)\n            sys.exit(1)\n        else:\n            project.core.ui.echo(\"[success]Installation succeeds.[/]\")\n\n\nclass RemoveCommand(BaseCommand):\n    \"\"\"Remove packages from PDM's environment\"\"\"\n\n    arguments = (verbose_option,)\n    name = \"remove\"\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\n            \"--pip-args\",\n            help=\"Arguments that will be passed to pip uninstall\",\n            default=\"\",\n        )\n        parser.add_argument(\"-y\", \"--yes\", action=\"store_true\", help=\"Answer yes on the question\")\n        parser.add_argument(\"packages\", nargs=\"+\", help=\"Specify one or many package names\")\n\n    def _resolve_dependencies_to_remove(self, packages: list[str]) -> list[str]:\n        \"\"\"Perform a BFS to find all unneeded dependencies\"\"\"\n        result: set[str] = set()\n        to_resolve = list(packages)\n\n        ws = WorkingSet()\n        graph = build_dependency_graph(ws, env_spec=EnvSpec.current())\n        while to_resolve:\n            temp: list[PackageNode] = []\n            for name in to_resolve:\n                key = normalize_name(name)\n                if key in ws:\n                    result.add(key)\n                package = PackageNode(key, \"0.0.0\", {})\n                if package not in graph:\n                    continue\n                for dep in graph.iter_children(package):\n                    temp.append(dep)\n                graph.remove(package)\n\n            to_resolve.clear()\n            for dep in temp:\n                if not any(graph.iter_parents(dep)) and dep.name != \"pdm\":\n                    to_resolve.append(dep.name)\n\n        return sorted(result)\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        packages_to_remove = self._resolve_dependencies_to_remove(options.packages)\n        if not packages_to_remove:\n            project.core.ui.echo(\"No package to remove.\", err=True)\n            sys.exit(1)\n        if not (options.yes or termui.confirm(f\"Will remove: {packages_to_remove}, continue?\", default=True)):\n            return\n        pip_args = [\"uninstall\", \"-y\", *shlex.split(options.pip_args), *packages_to_remove]\n\n        try:\n            with project.core.ui.open_spinner(f\"Uninstalling packages: [success]{', '.join(options.packages)}[/]\"):\n                run_pip(project, pip_args)\n        except subprocess.CalledProcessError as e:\n            project.core.ui.echo(\"[error]Uninstallation failed:[/]\\n\" + e.output, err=True)\n            sys.exit(1)\n        else:\n            project.core.ui.echo(\"[success]Uninstallation succeeds.[/]\")\n\n\nclass UpdateCommand(BaseCommand):\n    \"\"\"Update PDM itself\"\"\"\n\n    arguments = (verbose_option,)\n    name = \"update\"\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\n            \"--head\",\n            action=\"store_true\",\n            help=\"Update to the latest commit on the main branch\",\n        )\n        parser.add_argument(\n            \"--pre\",\n            help=\"Update to the latest prerelease version\",\n            action=\"store_true\",\n        )\n        parser.add_argument(\n            \"--no-frozen-deps\",\n            action=\"store_false\",\n            dest=\"frozen_deps\",\n            default=True,\n            help=\"Do not install frozen dependency versions\",\n        )\n        parser.add_argument(\n            \"--pip-args\",\n            help=\"Additional arguments that will be passed to pip install\",\n            default=\"\",\n        )\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        from pdm.__version__ import __version__, read_version\n\n        locked = \"[locked]\" if options.frozen_deps else \"\"\n\n        if options.head:\n            package = f\"pdm{locked} @ git+{PDM_REPO}@main\"\n            version: str | None = \"HEAD\"\n        else:\n            version = get_latest_pdm_version_from_pypi(project, options.pre)\n            assert version is not None, \"No version found\"\n            if parse_version(__version__) >= parse_version(version):\n                project.core.ui.echo(f\"Already up-to-date: [primary]{__version__}[/]\")\n                return\n            package = f\"pdm{locked}=={version}\"\n        pip_args = [\"install\", \"--upgrade\", \"--upgrade-strategy\", \"eager\", *shlex.split(options.pip_args), package]\n        try:\n            with project.core.ui.open_spinner(f\"Updating pdm to version [primary]{version}[/]\"):\n                run_pip(project, pip_args)\n        except subprocess.CalledProcessError as e:\n            project.core.ui.echo(\n                f\"[error]Installing version [primary]{version}[/] failed:[/]\\n\" + e.output,\n                err=True,\n            )\n            sys.exit(1)\n        else:\n            project.core.ui.echo(f\"[success]Successfully installed version [primary]{version}[/][/]\")\n            project.core.ui.echo(f\"See what's new in this version: [link]{PDM_REPO}/releases/tag/{version}[/]\")\n            # Update the version value to avoid check update print wrong message\n            project.core.version = read_version()\n"
  },
  {
    "path": "src/pdm/cli/commands/show.py",
    "content": "from __future__ import annotations\n\nimport argparse\nfrom typing import TYPE_CHECKING\n\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.options import venv_option\nfrom pdm.exceptions import PdmUsageError\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.project_info import ProjectInfo\nfrom pdm.models.requirements import parse_requirement\nfrom pdm.project import Project\nfrom pdm.utils import normalize_name, parse_version\n\nif TYPE_CHECKING:\n    from unearth import Package\n\n\ndef filter_stable(package: Package) -> bool:\n    assert package.version\n    return not parse_version(package.version).is_prerelease\n\n\nclass Command(BaseCommand):\n    \"\"\"Show the package information\"\"\"\n\n    metadata_keys = (\"name\", \"version\", \"summary\", \"license\", \"platform\", \"keywords\")\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        venv_option.add_to_parser(parser)\n        parser.add_argument(\n            \"package\",\n            type=normalize_name,\n            nargs=argparse.OPTIONAL,\n            help=\"Specify the package name, or show this package if not given\",\n        )\n        for option in self.metadata_keys:\n            parser.add_argument(f\"--{option}\", action=\"store_true\", help=f\"Show {option}\")\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        package = options.package\n        if package:\n            with project.environment.get_finder() as finder:\n                best_match = finder.find_best_match(package, allow_prereleases=True)\n            if not best_match.applicable:\n                project.core.ui.warn(f\"No match found for the package {package!r}\")\n                return\n            latest = Candidate.from_installation_candidate(best_match.best, parse_requirement(package))\n            latest_stable = next(filter(filter_stable, best_match.applicable), None)\n            metadata = latest.prepare(project.environment).metadata\n        else:\n            if not project.is_distribution:\n                raise PdmUsageError(\"This project is not a library\")\n            package = normalize_name(project.name)\n            metadata = project.make_self_candidate(False).prepare(project.environment).prepare_metadata(True)\n            latest_stable = None\n        project_info = ProjectInfo.from_distribution(metadata)\n\n        if any(getattr(options, key, None) for key in self.metadata_keys):\n            for key in self.metadata_keys:\n                if getattr(options, key, None):\n                    project.core.ui.echo(getattr(project_info, key))\n            return\n\n        installed = project.environment.get_working_set().get(package)\n        if latest_stable:\n            project_info.latest_stable_version = str(latest_stable.version)\n        if installed:\n            project_info.installed_version = str(installed.version)\n        project.core.ui.display_columns(list(project_info.generate_rows()))\n"
  },
  {
    "path": "src/pdm/cli/commands/sync.py",
    "content": "import argparse\n\nfrom pdm.cli import actions\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.filters import GroupSelection\nfrom pdm.cli.hooks import HookManager\nfrom pdm.cli.options import (\n    clean_group,\n    dry_run_option,\n    groups_group,\n    install_group,\n    lockfile_option,\n    skip_option,\n    venv_option,\n)\nfrom pdm.project import Project\n\n\nclass Command(BaseCommand):\n    \"\"\"Synchronize the current working set with lock file\"\"\"\n\n    arguments = (\n        *BaseCommand.arguments,\n        groups_group,\n        dry_run_option,\n        lockfile_option,\n        skip_option,\n        clean_group,\n        install_group,\n        venv_option,\n    )\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\n            \"-r\",\n            \"--reinstall\",\n            action=\"store_true\",\n            help=\"Force reinstall existing dependencies\",\n        )\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        actions.check_lockfile(project)\n        selection = GroupSelection.from_options(project, options)\n        actions.do_sync(\n            project,\n            selection=selection,\n            dry_run=options.dry_run,\n            clean=options.clean,\n            quiet=options.verbose == -1,\n            no_editable=options.no_editable,\n            no_self=options.no_self or \"default\" not in selection,\n            reinstall=options.reinstall,\n            only_keep=options.only_keep,\n            hooks=HookManager(project, options.skip),\n        )\n"
  },
  {
    "path": "src/pdm/cli/commands/update.py",
    "content": "from __future__ import annotations\n\nimport argparse\nfrom collections import defaultdict\nfrom typing import TYPE_CHECKING\n\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.filters import GroupSelection\nfrom pdm.cli.hooks import HookManager\nfrom pdm.cli.options import (\n    frozen_lockfile_option,\n    groups_group,\n    install_group,\n    lockfile_option,\n    override_option,\n    prerelease_option,\n    save_strategy_group,\n    skip_option,\n    unconstrained_option,\n    update_strategy_group,\n    venv_option,\n)\nfrom pdm.exceptions import PdmUsageError, ProjectError\n\nif TYPE_CHECKING:\n    from typing import Collection\n\n    from pdm.models.requirements import Requirement\n    from pdm.project import Project\n\n\nclass Command(BaseCommand):\n    \"\"\"Update package(s) in pyproject.toml\"\"\"\n\n    arguments = (\n        *BaseCommand.arguments,\n        groups_group,\n        install_group,\n        lockfile_option,\n        frozen_lockfile_option,\n        save_strategy_group,\n        override_option,\n        update_strategy_group,\n        prerelease_option,\n        unconstrained_option,\n        skip_option,\n        venv_option,\n    )\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\n            \"-t\",\n            \"--top\",\n            action=\"store_true\",\n            help=\"Only update those listed in pyproject.toml\",\n        )\n        parser.add_argument(\n            \"--dry-run\",\n            \"--outdated\",\n            action=\"store_true\",\n            dest=\"dry_run\",\n            help=\"Show the difference only without modifying the lockfile content\",\n        )\n        parser.add_argument(\n            \"--no-sync\",\n            dest=\"sync\",\n            default=True,\n            action=\"store_false\",\n            help=\"Only update lock file but do not sync packages\",\n        )\n        parser.add_argument(\"packages\", nargs=\"*\", help=\"If packages are given, only update them\")\n        parser.set_defaults(dev=None)\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        self.do_update(\n            project,\n            selection=GroupSelection.from_options(project, options),\n            save=options.save_strategy or project.config[\"strategy.save\"],\n            strategy=options.update_strategy or project.config[\"strategy.update\"],\n            unconstrained=options.unconstrained,\n            top=options.top,\n            dry_run=options.dry_run,\n            packages=options.packages,\n            sync=options.sync,\n            no_editable=options.no_editable,\n            no_self=options.no_self,\n            prerelease=options.prerelease,\n            fail_fast=options.fail_fast,\n            hooks=HookManager(project, options.skip),\n        )\n\n    @staticmethod\n    def do_update(\n        project: Project,\n        *,\n        selection: GroupSelection,\n        strategy: str = \"reuse\",\n        save: str = \"compatible\",\n        unconstrained: bool = False,\n        top: bool = False,\n        dry_run: bool = False,\n        packages: Collection[str] = (),\n        sync: bool = True,\n        no_editable: bool = False,\n        no_self: bool = False,\n        prerelease: bool | None = None,\n        fail_fast: bool = False,\n        hooks: HookManager | None = None,\n    ) -> None:\n        \"\"\"Update specified packages or all packages\"\"\"\n        from itertools import chain\n\n        from pdm.cli.actions import do_lock, do_sync\n        from pdm.cli.utils import check_project_file, save_version_specifiers\n        from pdm.models.specifiers import get_specifier\n        from pdm.utils import normalize_name\n\n        hooks = hooks or HookManager(project)\n        check_project_file(project)\n        if len(packages) > 0 and (top or len(selection.groups) > 1 or not selection.default):\n            raise PdmUsageError(\"packages argument can't be used together with multiple -G or --no-default or --top.\")\n        all_dependencies = project.all_dependencies\n        updated_deps: dict[str, list[Requirement]] = defaultdict(list)\n        locked_groups = project.lockfile.groups\n        tracked_names: set[str] = set()\n        if not packages:\n            if prerelease is not None:\n                raise PdmUsageError(\"--prerelease/--stable must be used with packages given\")\n            selection.validate()\n            for group in selection:\n                updated_deps[group] = list(all_dependencies[group])\n            tracked_names.update(r.identify() for deps in updated_deps.values() for r in deps)\n        else:\n            group = selection.one()\n            if locked_groups and group not in locked_groups:\n                raise ProjectError(f\"Requested group not in lockfile: {group}\")\n            dependencies = all_dependencies[group]\n            for name in packages:\n                normalized_name = normalize_name(name)\n                matched_reqs = [d for d in dependencies if (d.key or \"\") == normalized_name]\n                if not matched_reqs:\n                    candidates = project.get_locked_repository().all_candidates\n                    if normalized_name not in candidates:\n                        raise ProjectError(\n                            f\"[req]{name}[/] does not exist in [primary]{group}[/] \"\n                            f\"{'dev-' if selection.dev else ''}dependencies nor is a transitive dependency.\"\n                        )\n                    look_in_other_group = next(\n                        (\n                            g\n                            for g, deps in all_dependencies.items()\n                            if any(normalized_name == d.key for d in deps) and g != group\n                        ),\n                        None,\n                    )\n                    if look_in_other_group is not None:\n                        raise ProjectError(\n                            f\"[req]{name}[/] does not exist in [primary]{group}[/], but exists in [primary]{look_in_other_group}[/].\"\n                            \" Please specify the correct group with `-G/--group`.\"\n                        )\n                    tracked_names.add(normalized_name)\n                else:\n                    for req in matched_reqs:\n                        req.prerelease = prerelease\n                    updated_deps[group].extend(matched_reqs)\n            tracked_names.update(r.identify() for deps in updated_deps.values() for r in deps)\n            project.core.ui.echo(\n                f\"Updating {'[bold]global[/] ' if project.is_global else ''}packages: {', '.join(f'[req]{v}[/]' for v in tracked_names)}.\"\n            )\n        if unconstrained:\n            for deps in updated_deps.values():\n                for dep in deps:\n                    dep.specifier = get_specifier(\"\")\n        reqs = [r for g, deps in all_dependencies.items() for r in deps if locked_groups is None or g in locked_groups]\n        # Since dry run is always true in the locking,\n        # we need to emit the hook manually with the real dry_run value\n        hooks.try_emit(\"pre_lock\", requirements=reqs, dry_run=dry_run)\n        with hooks.skipping(\"pre_lock\", \"post_lock\"):\n            resolved = do_lock(\n                project,\n                strategy=strategy,\n                tracked_names=tracked_names,\n                requirements=reqs,\n                dry_run=True,\n                hooks=hooks,\n                groups=locked_groups,\n            )\n        if unconstrained:\n            # Need to update version constraints\n            save_version_specifiers(chain.from_iterable(updated_deps.values()), resolved, save)\n        if not dry_run:\n            if unconstrained:\n                for group, deps in updated_deps.items():\n                    project.add_dependencies(deps, group, selection.dev or False)\n            project.write_lockfile(show_message=False)\n        hooks.try_emit(\"post_lock\", resolution=resolved, dry_run=dry_run)\n        if sync or dry_run:\n            do_sync(\n                project,\n                selection=selection,\n                clean=False,\n                dry_run=dry_run,\n                requirements=[r for deps in updated_deps.values() for r in deps],\n                tracked_names=[r.identify() for deps in updated_deps.values() for r in deps] if top else None,\n                no_editable=no_editable,\n                no_self=no_self or \"default\" not in selection,\n                fail_fast=fail_fast,\n                hooks=hooks,\n            )\n"
  },
  {
    "path": "src/pdm/cli/commands/use.py",
    "content": "from __future__ import annotations\n\nimport argparse\n\nfrom pdm import termui\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.hooks import HookManager\nfrom pdm.cli.options import skip_option\nfrom pdm.exceptions import NoPythonVersion\nfrom pdm.models.caches import JSONFileCache\nfrom pdm.models.python import PythonInfo\nfrom pdm.models.venv import get_venv_python\nfrom pdm.project import Project\nfrom pdm.utils import is_conda_base_python\n\n\nclass Command(BaseCommand):\n    \"\"\"Use the given python version or path as base interpreter. If not found, PDM will try to install one.\"\"\"\n\n    def add_arguments(self, parser: argparse.ArgumentParser) -> None:\n        skip_option.add_to_parser(parser)\n        unattended_use_group = parser.add_mutually_exclusive_group()\n        unattended_use_group.add_argument(\n            \"-f\",\n            \"--first\",\n            action=\"store_true\",\n            help=\"Select the first matched interpreter - no auto install\",\n        )\n        unattended_use_group.add_argument(\n            \"--auto-install-min\",\n            action=\"store_true\",\n            help=\"If `python` argument not given, auto install minimal best match - otherwise has no effect\",\n        )\n        unattended_use_group.add_argument(\n            \"--auto-install-max\",\n            action=\"store_true\",\n            help=\"If `python` argument not given, auto install maximum best match - otherwise has no effect\",\n        )\n        parser.add_argument(\n            \"-i\",\n            \"--ignore-remembered\",\n            action=\"store_true\",\n            help=\"Ignore the remembered selection\",\n        )\n        parser.add_argument(\n            \"--no-version-file\",\n            dest=\"version_file\",\n            default=True,\n            action=\"store_false\",\n            help=\"Do not write .python-version file\",\n        )\n        parser.add_argument(\"--venv\", help=\"Use the interpreter in the virtual environment with the given name\")\n        parser.add_argument(\"python\", nargs=\"?\", help=\"Specify the Python version or path\", default=\"\")\n\n    @staticmethod\n    def select_python(\n        project: Project,\n        python: str,\n        *,\n        ignore_remembered: bool,\n        ignore_requires_python: bool,\n        venv: str | None,\n        first: bool,\n        auto_install_min: bool,\n        auto_install_max: bool,\n    ) -> PythonInfo:\n        from pdm.cli.commands.python import InstallCommand\n        from pdm.cli.commands.venv.utils import get_venv_with_name\n\n        def version_matcher(py_version: PythonInfo) -> bool:\n            return py_version.valid and (\n                ignore_requires_python or project.python_requires.contains(py_version.version, True)\n            )\n\n        if venv:\n            virtualenv = get_venv_with_name(project, venv)\n            return PythonInfo.from_path(virtualenv.interpreter)\n\n        if not project.cache_dir.exists():\n            project.cache_dir.mkdir(parents=True)\n        use_cache: JSONFileCache[str, str] = JSONFileCache(project.cache_dir / \"use_cache.json\")\n        python = python.strip()\n        if python and not ignore_remembered and python in use_cache:\n            path = use_cache.get(python)\n            cached_python = PythonInfo.from_path(path)\n            if not cached_python.valid:\n                project.core.ui.error(\n                    f\"The last selection is corrupted. {path!r}\",\n                )\n            elif version_matcher(cached_python):\n                project.core.ui.info(\"Using the last selection, add '-i' to ignore it.\")\n                return cached_python\n\n        if not python and not first and (auto_install_min or auto_install_max):\n            match = project.get_best_matching_cpython_version(auto_install_min)\n            if match is None:\n                req = f'requires-python=\"{project.python_requires}\"'\n                raise NoPythonVersion(\n                    f\"No Python interpreter matching [success]{req}[/] is found based on 'auto-install' strategy.\"\n                )\n            try:\n                installed_interpreter_to_use = InstallCommand.install_python(project, str(match))\n            except Exception as e:\n                project.core.ui.error(f\"Failed to install Python {python}: {e}\")\n                project.core.ui.info(\"Please select a Python interpreter manually\")\n            else:\n                return installed_interpreter_to_use\n\n        found_interpreters = list(\n            dict.fromkeys(project.iter_interpreters(python, filter_func=version_matcher, respect_version_file=False))\n        )\n        if not found_interpreters:\n            req = python if ignore_requires_python else f'requires-python=\"{project.python_requires}\"'\n            raise NoPythonVersion(f\"No Python interpreter matching [success]{req}[/] is found.\")\n\n        if first or len(found_interpreters) == 1 or not termui.is_interactive():\n            project.core.ui.info(\"Using the first matched interpreter.\")\n            return found_interpreters[0]\n\n        project.core.ui.echo(\n            f\"Please enter the {'[bold]Global[/] ' if project.is_global else ''}Python interpreter to use\"\n        )\n        for i, py_version in enumerate(found_interpreters):\n            project.core.ui.echo(\n                f\"{i:>2}. [success]{py_version.implementation}@{py_version.identifier}[/] ({py_version.path!s})\"\n            )\n        selection = termui.ask(\n            \"Please select\",\n            default=\"0\",\n            prompt_type=int,\n            choices=[str(i) for i in range(len(found_interpreters))],\n            show_choices=False,\n        )\n        return found_interpreters[int(selection)]\n\n    def do_use(\n        self,\n        project: Project,\n        python: str = \"\",\n        first: bool = False,\n        ignore_remembered: bool = False,\n        ignore_requires_python: bool = False,\n        save: bool = True,\n        venv: str | None = None,\n        auto_install_min: bool = False,\n        auto_install_max: bool = False,\n        version_file: bool = True,\n        hooks: HookManager | None = None,\n    ) -> PythonInfo:\n        \"\"\"Use the specified python version and save in project config.\n        The python can be a version string or interpreter path.\n        \"\"\"\n        from pdm.environments import PythonLocalEnvironment\n\n        selected_python = self.select_python(\n            project,\n            python,\n            ignore_remembered=ignore_remembered,\n            first=first,\n            venv=venv,\n            ignore_requires_python=ignore_requires_python,\n            auto_install_min=auto_install_min,\n            auto_install_max=auto_install_max,\n        )\n        # NOTE: PythonInfo is cached with path as key.\n        # This can lead to inconsistency when the same virtual environment is reused.\n        # So the original python identifier is preserved here for logging purpose.\n        selected_python_identifier = selected_python.identifier\n        if python and selected_python.get_venv() is None:\n            use_cache: JSONFileCache[str, str] = JSONFileCache(project.cache_dir / \"use_cache.json\")\n            use_cache.set(python, selected_python.path.as_posix())\n\n        if project.config[\"python.use_venv\"] and (\n            selected_python.get_venv() is None or is_conda_base_python(selected_python.path)\n        ):\n            venv_path = project._create_virtualenv(str(selected_python.path))\n            selected_python = PythonInfo.from_path(get_venv_python(venv_path))\n        if not save:\n            return selected_python\n\n        saved_python = project._saved_python\n        old_python = PythonInfo.from_path(saved_python) if saved_python else None\n        project.core.ui.echo(\n            f\"Using {'[bold]Global[/] ' if project.is_global else ''}Python interpreter: [success]{selected_python.path!s}[/] ({selected_python_identifier})\"\n        )\n        project.python = selected_python\n        if version_file and project.config[\"python.use_python_version\"]:\n            with project.root.joinpath(\".python-version\").open(\"w\") as f:\n                f.write(f\"{selected_python.major}.{selected_python.minor}\\n\")\n        if project.environment.is_local:\n            assert isinstance(project.environment, PythonLocalEnvironment)\n            project.core.ui.echo(\n                \"Using __pypackages__ because non-venv Python is used.\",\n                style=\"primary\",\n                err=True,\n            )\n            if old_python and old_python.executable != selected_python.executable:\n                project.core.ui.echo(\"Updating executable scripts...\", style=\"primary\")\n                project.environment.update_shebangs(selected_python.executable.as_posix())\n\n        hooks = hooks or HookManager(project)\n        hooks.try_emit(\"post_use\", python=selected_python)\n        return selected_python\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        self.do_use(\n            project,\n            python=options.python,\n            first=options.first,\n            ignore_remembered=options.ignore_remembered,\n            venv=options.venv,\n            auto_install_min=options.auto_install_min,\n            auto_install_max=options.auto_install_max,\n            version_file=options.version_file,\n            hooks=HookManager(project, options.skip),\n        )\n"
  },
  {
    "path": "src/pdm/cli/commands/venv/__init__.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.commands.venv.activate import ActivateCommand\nfrom pdm.cli.commands.venv.create import CreateCommand\nfrom pdm.cli.commands.venv.list import ListCommand\nfrom pdm.cli.commands.venv.purge import PurgeCommand\nfrom pdm.cli.commands.venv.remove import RemoveCommand\nfrom pdm.cli.commands.venv.utils import get_venv_with_name\nfrom pdm.cli.options import project_option\n\nif TYPE_CHECKING:\n    from argparse import ArgumentParser, Namespace\n\n    from pdm.project import Project\n\n\nclass Command(BaseCommand):\n    \"\"\"Virtualenv management\"\"\"\n\n    name = \"venv\"\n    arguments = (project_option,)\n\n    def add_arguments(self, parser: ArgumentParser) -> None:\n        group = parser.add_mutually_exclusive_group()\n        group.add_argument(\"--path\", help=\"Show the path to the given virtualenv\")\n        group.add_argument(\"--python\", help=\"Show the python interpreter path for the given virtualenv\")\n        subparser = parser.add_subparsers(title=\"commands\", metavar=\"\")\n        CreateCommand.register_to(subparser, \"create\")\n        ListCommand.register_to(subparser, \"list\")\n        RemoveCommand.register_to(subparser, \"remove\")\n        ActivateCommand.register_to(subparser, \"activate\")\n        PurgeCommand.register_to(subparser, \"purge\")\n        self.parser = parser\n\n    def handle(self, project: Project, options: Namespace) -> None:\n        if options.path:\n            venv = get_venv_with_name(project, options.path)\n            project.core.ui.echo(str(venv.root))\n        elif options.python:\n            venv = get_venv_with_name(project, options.python)\n            project.core.ui.echo(str(venv.interpreter))\n        else:\n            self.parser.print_help()\n"
  },
  {
    "path": "src/pdm/cli/commands/venv/activate.py",
    "content": "from __future__ import annotations\n\nimport platform\nimport shlex\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nimport shellingham\n\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.commands.venv.utils import get_venv_with_name\nfrom pdm.cli.options import verbose_option\nfrom pdm.models.venv import VirtualEnv\n\nif TYPE_CHECKING:\n    from argparse import ArgumentParser, Namespace\n\n    from pdm.project import Project\n\n\nclass ActivateCommand(BaseCommand):\n    \"\"\"Print the command to activate the virtualenv with the given name\"\"\"\n\n    arguments = (verbose_option,)\n\n    def add_arguments(self, parser: ArgumentParser) -> None:\n        parser.add_argument(\"env\", nargs=\"?\", help=\"The key of the virtualenv\")\n\n    def handle(self, project: Project, options: Namespace) -> None:\n        if options.env:\n            venv = get_venv_with_name(project, options.env)\n        else:\n            # Use what is saved in .pdm-python\n            interpreter = project._saved_python\n            if not interpreter:\n                project.core.ui.warn(\n                    \"The project doesn't have a saved python.path. Run [success]pdm use[/] to pick one.\"\n                )\n                raise SystemExit(1)\n            venv_like = VirtualEnv.from_interpreter(Path(interpreter))\n            if venv_like is None:\n                project.core.ui.warn(\n                    f\"Can't activate a non-venv Python [success]{interpreter}[/], \"\n                    \"you can specify one with [success]pdm venv activate <env_name>[/]\",\n                )\n                raise SystemExit(1)\n            venv = venv_like\n        project.core.ui.echo(self.get_activate_command(venv))\n\n    def get_activate_command(self, venv: VirtualEnv) -> str:  # pragma: no cover\n        try:\n            shell, _ = shellingham.detect_shell()\n        except shellingham.ShellDetectionFailure:\n            shell = \"\"\n        if shell == \"fish\":\n            command, filename = \"source\", \"activate.fish\"\n        elif shell in [\"csh\", \"tcsh\"]:\n            command, filename = \"source\", \"activate.csh\"\n        elif shell in [\"powershell\", \"pwsh\"]:\n            command, filename = \".\", \"Activate.ps1\"\n        else:\n            command, filename = \"source\", \"activate\"\n        activate_script = venv.interpreter.with_name(filename)\n        if activate_script.exists():\n            if platform.system() == \"Windows\":\n                return f\"{self.quote(str(activate_script), shell)}\"\n            return f\"{command} {self.quote(str(activate_script), shell)}\"\n        # Conda backed virtualenvs don't have activate scripts\n        return f\"conda activate {self.quote(str(venv.root), shell)}\"\n\n    @staticmethod\n    def quote(command: str, shell: str) -> str:\n        if shell in [\"powershell\", \"pwsh\"] or platform.system() == \"Windows\":\n            return \"{}\".format(command.replace(\"'\", \"''\"))\n        return shlex.quote(command)\n"
  },
  {
    "path": "src/pdm/cli/commands/venv/backends.py",
    "content": "from __future__ import annotations\n\nimport abc\nimport os\nimport shutil\nimport subprocess\nimport sys\nfrom functools import cached_property\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any\n\nfrom pdm import termui\nfrom pdm.cli.commands.venv.utils import get_venv_prefix\nfrom pdm.exceptions import PdmUsageError, ProjectError\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable, Mapping\n\n    from pdm.models.python import PythonInfo\n    from pdm.project import Project\n\n\nclass VirtualenvCreateError(ProjectError):\n    pass\n\n\nclass Backend(abc.ABC):\n    \"\"\"The base class for virtualenv backends\"\"\"\n\n    def __init__(self, project: Project, python: str | None) -> None:\n        self.project = project\n        self.python = python\n\n    @abc.abstractmethod\n    def pip_args(self, with_pip: bool) -> Iterable[str]:\n        pass\n\n    @cached_property\n    def _resolved_interpreter(self) -> PythonInfo:\n        if not self.python:\n            project_python = self.project._python\n            if project_python:\n                return project_python\n\n        def match_func(py_version: PythonInfo) -> bool:\n            return bool(self.python) or (\n                py_version.valid and self.project.python_requires.contains(py_version.version, True)\n            )\n\n        respect_version_file = self.project.config[\"python.use_python_version\"]\n        for py_version in self.project.iter_interpreters(\n            self.python, search_venv=False, filter_func=match_func, respect_version_file=respect_version_file\n        ):\n            return py_version\n\n        python = f\" {self.python}\" if self.python else \"\"\n        raise VirtualenvCreateError(f\"Can't resolve python interpreter{python}\")\n\n    @property\n    def ident(self) -> str:\n        \"\"\"Get the identifier of this virtualenv.\n        self.python can be one of:\n            3.8\n            /usr/bin/python\n            3.9.0a4\n            python3.8\n        \"\"\"\n        return self._resolved_interpreter.identifier\n\n    def subprocess_call(self, cmd: list[str], **kwargs: Any) -> None:\n        self.project.core.ui.echo(\n            f\"Run command: [success]{cmd}[/]\",\n            verbosity=termui.Verbosity.DETAIL,\n            err=True,\n        )\n        try:\n            subprocess.check_call(\n                cmd,\n                stdout=subprocess.DEVNULL if self.project.core.ui.verbosity < termui.Verbosity.DETAIL else None,\n            )\n        except subprocess.CalledProcessError as e:  # pragma: no cover\n            raise VirtualenvCreateError(e) from None\n\n    def _ensure_clean(self, location: Path, force: bool = False) -> None:\n        if not location.exists():\n            return\n        if location.is_dir() and not any(location.iterdir()):\n            return\n        if not force:\n            raise VirtualenvCreateError(f\"The location {location} is not empty, add --force to overwrite it.\")\n        if location.is_file():\n            self.project.core.ui.info(f\"Removing existing file {location}\", verbosity=termui.Verbosity.DETAIL)\n            location.unlink()\n        else:\n            self.project.core.ui.info(\n                f\"Cleaning existing target directory {location}\", verbosity=termui.Verbosity.DETAIL\n            )\n            with os.scandir(location) as entries:\n                for entry in entries:\n                    if entry.is_dir() and not entry.is_symlink():\n                        shutil.rmtree(entry.path)\n                    else:\n                        os.remove(entry.path)\n\n    def get_location(self, name: str | None = None, venv_name: str | None = None) -> Path:\n        if name and venv_name:\n            raise PdmUsageError(\"Cannot specify both name and venv_name\")\n        venv_parent = Path(self.project.config[\"venv.location\"]).expanduser()\n        if not venv_parent.is_dir():\n            venv_parent.mkdir(exist_ok=True, parents=True)\n        if not venv_name:\n            venv_name = f\"{get_venv_prefix(self.project)}{name or self.ident}\"\n        return venv_parent / venv_name\n\n    def create(\n        self,\n        name: str | None = None,\n        args: tuple[str, ...] = (),\n        force: bool = False,\n        in_project: bool = False,\n        prompt: str | None = None,\n        with_pip: bool = False,\n        venv_name: str | None = None,\n    ) -> Path:\n        location = (self.project.root / \".venv\") if in_project else self.get_location(name, venv_name)\n        args = (*self.pip_args(with_pip), *args)\n        if prompt is not None:\n            prompt = prompt.format(\n                project_name=self.project.root.name.lower() or \"virtualenv\",\n                python_version=self.ident,\n            )\n        self._ensure_clean(location, force)\n        self.perform_create(location, args, prompt=prompt)\n        return location\n\n    @abc.abstractmethod\n    def perform_create(self, location: Path, args: tuple[str, ...], prompt: str | None = None) -> None:\n        pass\n\n\nclass VirtualenvBackend(Backend):\n    def pip_args(self, with_pip: bool) -> Iterable[str]:\n        if with_pip:\n            return ()\n        return (\"--no-pip\", \"--no-setuptools\", \"--no-wheel\")\n\n    def perform_create(self, location: Path, args: tuple[str, ...], prompt: str | None = None) -> None:\n        prompt_option = (f\"--prompt={prompt}\",) if prompt else ()\n        cmd = [\n            sys.executable,\n            \"-m\",\n            \"virtualenv\",\n            str(location),\n            \"-p\",\n            str(self._resolved_interpreter.executable),\n            *prompt_option,\n            *args,\n        ]\n        self.subprocess_call(cmd)\n\n\nclass VenvBackend(VirtualenvBackend):\n    def pip_args(self, with_pip: bool) -> Iterable[str]:\n        if with_pip:\n            return ()\n        return (\"--without-pip\",)\n\n    def perform_create(self, location: Path, args: tuple[str, ...], prompt: str | None = None) -> None:\n        prompt_option = (f\"--prompt={prompt}\",) if prompt else ()\n        cmd = [str(self._resolved_interpreter.executable), \"-m\", \"venv\", str(location), *prompt_option, *args]\n        self.subprocess_call(cmd)\n\n\nclass UvBackend(VirtualenvBackend):\n    def pip_args(self, with_pip: bool) -> Iterable[str]:\n        if with_pip:\n            return (\"--seed\",)\n        return ()\n\n    def perform_create(self, location: Path, args: tuple[str, ...], prompt: str | None = None) -> None:\n        prompt_option = (f\"--prompt={prompt}\",) if prompt else ()\n        cmd = [\n            *self.project.core.uv_cmd,\n            \"venv\",\n            \"-p\",\n            str(self._resolved_interpreter.executable),\n            *prompt_option,\n            *args,\n            str(location),\n        ]\n        self.subprocess_call(cmd)\n\n\nclass CondaBackend(Backend):\n    @property\n    def ident(self) -> str:\n        # Conda supports specifying python that doesn't exist,\n        # use the passed-in name directly\n        if self.python:\n            return self.python\n        return super().ident\n\n    def pip_args(self, with_pip: bool) -> Iterable[str]:\n        if with_pip:\n            return (\"pip\",)\n        return ()\n\n    def perform_create(self, location: Path, args: tuple[str, ...], prompt: str | None = None) -> None:\n        if self.python:\n            python_ver = self.python\n        else:\n            python = self._resolved_interpreter\n            python_ver = f\"{python.major}.{python.minor}\"\n        if any(arg.startswith(\"python=\") for arg in args):\n            raise PdmUsageError(\"Cannot use python= in conda creation arguments\")\n\n        cmd = [\"conda\", \"create\", \"--yes\", \"--prefix\", str(location), f\"python={python_ver}\", *args]\n        self.subprocess_call(cmd)\n\n\nBACKENDS: Mapping[str, type[Backend]] = {\n    \"virtualenv\": VirtualenvBackend,\n    \"venv\": VenvBackend,\n    \"conda\": CondaBackend,\n    \"uv\": UvBackend,\n}\n"
  },
  {
    "path": "src/pdm/cli/commands/venv/create.py",
    "content": "from __future__ import annotations\n\nimport argparse\nfrom typing import TYPE_CHECKING\n\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.commands.venv.backends import BACKENDS\nfrom pdm.cli.options import verbose_option\n\nif TYPE_CHECKING:\n    from argparse import ArgumentParser, Namespace\n\n    from pdm.project import Project\n\n\nclass CreateCommand(BaseCommand):\n    \"\"\"Create a virtualenv\n\n    pdm venv create <python> [-other args]\n    \"\"\"\n\n    description = \"Create a virtualenv\"\n    arguments = (verbose_option,)\n\n    def add_arguments(self, parser: ArgumentParser) -> None:\n        parser.add_argument(\n            \"-w\",\n            \"--with\",\n            dest=\"backend\",\n            choices=BACKENDS.keys(),\n            help=\"Specify the backend to create the virtualenv\",\n        )\n        parser.add_argument(\n            \"-f\",\n            \"--force\",\n            action=\"store_true\",\n            help=\"Recreate if the virtualenv already exists\",\n        )\n        parser.add_argument(\"-n\", \"--name\", help=\"Specify the name of the virtualenv\")\n        parser.add_argument(\"--with-pip\", action=\"store_true\", help=\"Install pip with the virtualenv\")\n        parser.add_argument(\n            \"python\",\n            nargs=\"?\",\n            help=\"Specify which python should be used to create the virtualenv\",\n        )\n        parser.add_argument(\n            \"venv_args\",\n            nargs=argparse.REMAINDER,\n            help=\"Additional arguments that will be passed to the backend\",\n        )\n\n    def handle(self, project: Project, options: Namespace) -> None:\n        in_project = project.config[\"venv.in_project\"] and not options.name\n        backend: str = options.backend or project.config[\"venv.backend\"]\n        venv_backend = BACKENDS[backend](project, options.python)\n        with project.core.ui.open_spinner(f\"Creating virtualenv using [success]{backend}[/]...\"):\n            path = venv_backend.create(\n                options.name,\n                options.venv_args,\n                options.force,\n                in_project,\n                prompt=project.config[\"venv.prompt\"],\n                with_pip=options.with_pip or project.config[\"venv.with_pip\"],\n            )\n        project.core.ui.echo(f\"Virtualenv [success]{path}[/] is created successfully\")\n"
  },
  {
    "path": "src/pdm/cli/commands/venv/list.py",
    "content": "from __future__ import annotations\n\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.commands.venv.utils import iter_venvs\nfrom pdm.cli.options import verbose_option\n\nif TYPE_CHECKING:\n    from argparse import Namespace\n\n    from pdm.project import Project\n\n\nclass ListCommand(BaseCommand):\n    \"\"\"List all virtualenvs associated with this project\"\"\"\n\n    arguments = (verbose_option,)\n\n    def handle(self, project: Project, options: Namespace) -> None:\n        project.core.ui.echo(\"Virtualenvs created with this project:\\n\")\n        saved_python_root = Path(saved_python).parent.parent if (saved_python := project._saved_python) else None\n        for ident, venv in iter_venvs(project):\n            mark = \"*\" if saved_python_root and saved_python_root == venv.root else \"-\"\n            project.core.ui.echo(f\"{mark}  [success]{ident}[/]: {venv.root}\")\n"
  },
  {
    "path": "src/pdm/cli/commands/venv/purge.py",
    "content": "from __future__ import annotations\n\nimport shutil\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nfrom pdm import termui\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.commands.venv.utils import iter_central_venvs\nfrom pdm.cli.options import verbose_option\n\nif TYPE_CHECKING:\n    from argparse import ArgumentParser, Namespace\n\n    from pdm.project import Project\n\n\nclass PurgeCommand(BaseCommand):\n    \"\"\"Purge selected/all created Virtualenvs\"\"\"\n\n    arguments = (verbose_option,)\n\n    def add_arguments(self, parser: ArgumentParser) -> None:\n        parser.add_argument(\n            \"-f\",\n            \"--force\",\n            action=\"store_true\",\n            help=\"Force purging without prompting for confirmation\",\n        )\n        parser.add_argument(\n            \"-i\",\n            \"--interactive\",\n            action=\"store_true\",\n            help=\"Interactively purge selected Virtualenvs\",\n        )\n\n    def handle(self, project: Project, options: Namespace) -> None:\n        all_central_venvs = list(iter_central_venvs(project))\n        if not all_central_venvs:\n            project.core.ui.echo(\"No virtualenvs to purge, quitting.\", style=\"success\")\n            return\n\n        if not options.force:\n            project.core.ui.echo(\"The following Virtualenvs will be purged:\", style=\"warning\")\n            for i, venv in enumerate(all_central_venvs):\n                project.core.ui.echo(f\"{i}. [success]{venv[0]}[/]\")\n\n        if not options.interactive and (options.force or termui.confirm(\"continue?\", default=True)):\n            return self.del_all_venvs(project)\n\n        selection = termui.ask(\n            \"Please select\",\n            choices=([str(i) for i in range(len(all_central_venvs))] + [\"all\", \"none\"]),\n            default=\"none\",\n            show_choices=False,\n        )\n\n        if selection == \"all\":\n            self.del_all_venvs(project)\n        elif selection != \"none\":\n            for i, venv in enumerate(all_central_venvs):\n                if i == int(selection):\n                    shutil.rmtree(venv[1])\n            project.core.ui.echo(\"Purged successfully!\")\n\n    def del_all_venvs(self, project: Project) -> None:\n        saved_python = project._saved_python\n        for _, venv in iter_central_venvs(project):\n            shutil.rmtree(venv)\n            if saved_python and Path(saved_python).parent.parent == venv:\n                project._saved_python = None\n        project.core.ui.echo(\"Purged successfully!\")\n"
  },
  {
    "path": "src/pdm/cli/commands/venv/remove.py",
    "content": "from __future__ import annotations\n\nimport shutil\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nfrom pdm import termui\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.cli.commands.venv.utils import get_venv_with_name\nfrom pdm.cli.options import verbose_option\n\nif TYPE_CHECKING:\n    from argparse import ArgumentParser, Namespace\n\n    from pdm.project import Project\n\n\nclass RemoveCommand(BaseCommand):\n    \"\"\"Remove the virtualenv with the given name\"\"\"\n\n    arguments = (verbose_option,)\n\n    def add_arguments(self, parser: ArgumentParser) -> None:\n        parser.add_argument(\n            \"-y\",\n            \"--yes\",\n            action=\"store_true\",\n            help=\"Answer yes on the following question\",\n        )\n        parser.add_argument(\"env\", help=\"The key of the virtualenv\")\n\n    def handle(self, project: Project, options: Namespace) -> None:\n        project.core.ui.echo(\"Virtualenvs created with this project:\")\n        venv = get_venv_with_name(project, options.env)\n        if options.yes or termui.confirm(f\"[warning]Will remove: [success]{venv.root}[/], continue?\", default=True):\n            shutil.rmtree(venv.root)\n            saved_python = project._saved_python\n            if saved_python and Path(saved_python).parent.parent == venv.root:\n                project._saved_python = None\n            project.core.ui.echo(\"Removed successfully!\")\n"
  },
  {
    "path": "src/pdm/cli/commands/venv/utils.py",
    "content": "from __future__ import annotations\n\nimport base64\nimport hashlib\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nfrom findpython import BaseProvider, PythonVersion\n\nfrom pdm.exceptions import PdmUsageError\nfrom pdm.models.venv import VirtualEnv\n\nif TYPE_CHECKING:\n    import sys\n    from collections.abc import Iterable\n\n    if sys.version_info >= (3, 11):\n        from typing import Self\n    else:\n        from typing_extensions import Self\n\n    from pdm.project import Project\n\n\ndef hash_path(path: str) -> str:\n    \"\"\"Generate a hash for the given path.\"\"\"\n    return base64.urlsafe_b64encode(hashlib.new(\"md5\", path.encode(), usedforsecurity=False).digest()).decode()[:8]\n\n\ndef get_in_project_venv(root: Path) -> VirtualEnv | None:\n    \"\"\"Get the python interpreter path of venv-in-project\"\"\"\n    for possible_dir in (\".venv\", \"venv\", \"env\"):\n        venv = VirtualEnv.get(root / possible_dir)\n        if venv is not None:\n            return venv\n    return None\n\n\ndef get_venv_prefix(project: Project) -> str:\n    \"\"\"Get the venv prefix for the project\"\"\"\n    path = project.root\n    name_hash = hash_path(path.as_posix())\n    return f\"{path.name}-{name_hash}-\"\n\n\ndef iter_venvs(project: Project) -> Iterable[tuple[str, VirtualEnv]]:\n    \"\"\"Return an iterable of venv paths associated with the project\"\"\"\n    in_project_venv = get_in_project_venv(project.root)\n    if in_project_venv is not None:\n        yield \"in-project\", in_project_venv\n    venv_prefix = get_venv_prefix(project)\n    venv_parent = Path(project.config[\"venv.location\"])\n    for path in venv_parent.glob(f\"{venv_prefix}*\"):\n        ident = path.name[len(venv_prefix) :]\n        venv = VirtualEnv.get(path)\n        if venv is not None:\n            yield ident, venv\n\n\ndef iter_central_venvs(project: Project) -> Iterable[tuple[str, Path]]:\n    \"\"\"Return an iterable of all managed venvs and their paths.\"\"\"\n    venv_parent = Path(project.config[\"venv.location\"])\n    for venv in venv_parent.glob(\"*\"):\n        ident = venv.name\n        yield ident, venv\n\n\nclass VenvProvider(BaseProvider):\n    \"\"\"A Python provider for project venv pythons\"\"\"\n\n    def __init__(self, project: Project) -> None:\n        self.project = project\n\n    @classmethod\n    def create(cls) -> Self | None:\n        return None\n\n    def find_pythons(self) -> Iterable[PythonVersion]:\n        for _, venv in iter_venvs(self.project):\n            yield PythonVersion(venv.interpreter, _interpreter=venv.interpreter, keep_symlink=True)\n\n\ndef get_venv_with_name(project: Project, name: str) -> VirtualEnv:\n    all_venvs = dict(iter_venvs(project))\n    try:\n        return all_venvs[name]\n    except KeyError:\n        raise PdmUsageError(\n            f\"No virtualenv with key '{name}' is found, must be one of {list(all_venvs)}.\\n\"\n            \"You can create one with 'pdm venv create'.\",\n        ) from None\n"
  },
  {
    "path": "src/pdm/cli/completions/__init__.py",
    "content": ""
  },
  {
    "path": "src/pdm/cli/completions/pdm.bash",
    "content": "# BASH completion script for pdm\n# Generated by pycomplete 0.4.0\n\n_pdm_a919b69078acdf0a_complete()\n{\n    local cur script coms opts com\n    COMPREPLY=()\n    _get_comp_words_by_ref -n : cur words\n\n    # for an alias, get the real script behind it\n    if [[ $(type -t ${words[0]}) == \"alias\" ]]; then\n        script=$(alias ${words[0]} | sed -E \"s/alias ${words[0]}='(.*)'/\\\\1/\")\n    else\n        script=${words[0]}\n    fi\n\n    # lookup for command\n    for word in ${words[@]:1}; do\n        if [[ $word != -* ]]; then\n            com=$word\n            break\n        fi\n    done\n\n    # completing for an option\n    if [[ ${cur} == --* ]] ; then\n        opts=\"--config --help --ignore-python --no-cache --non-interactive --pep582 --quiet --verbose --version\"\n\n        case \"$com\" in\n\n            (add)\n            opts=\"--config-setting --dev --dry-run --editable --fail-fast --frozen-lockfile --global --group --help --lockfile --no-editable --no-isolation --no-self --no-sync --override --prerelease --project --quiet --save-compatible --save-exact --save-minimum --save-safe-compatible --save-wildcard --skip --stable --unconstrained --update-all --update-eager --update-reuse --update-reuse-installed --venv --verbose\"\n            ;;\n\n            (build)\n            opts=\"--config-setting --dest --help --no-clean --no-isolation --no-sdist --no-wheel --project --quiet --skip --verbose\"\n            ;;\n\n            (cache)\n            opts=\"--help --quiet --verbose\"\n            ;;\n\n            (completion)\n            opts=\"--help\"\n            ;;\n\n            (config)\n            opts=\"--delete --edit --global --help --local --project --quiet --verbose\"\n            ;;\n\n            (export)\n            opts=\"--dev --editable-self --expandvars --format --global --group --help --lockfile --no-default --no-extras --no-markers --output --production --project --pyproject --quiet --self --verbose --without --without-hashes\"\n            ;;\n\n            (fix)\n            opts=\"--dry-run --global --help --project --quiet --verbose\"\n            ;;\n\n            (import)\n            opts=\"--dev --format --global --group --help --project --quiet --verbose\"\n            ;;\n\n            (info)\n            opts=\"--env --global --help --json --packages --project --python --quiet --venv --verbose --where\"\n            ;;\n\n            (init)\n            opts=\"--backend --cookiecutter --copier --dist --global --help --license --name --no-git --non-interactive --overwrite --project --project-version --python --quiet --skip --verbose\"\n            ;;\n\n            (install)\n            opts=\"--check --config-setting --dev --dry-run --fail-fast --frozen-lockfile --global --group --help --lockfile --no-default --no-editable --no-isolation --no-self --override --plugins --production --project --quiet --skip --venv --verbose --without\"\n            ;;\n\n            (list)\n            opts=\"--csv --exclude --fields --freeze --global --graph --help --include --json --markdown --project --quiet --resolve --reverse --sort --venv --verbose\"\n            ;;\n\n            (lock)\n            opts=\"--append --check --config-setting --dev --exclude-newer --global --group --help --implementation --lockfile --no-cross-platform --no-default --no-isolation --no-static-urls --override --platform --production --project --python --quiet --refresh --skip --static-urls --strategy --update-reuse --update-reuse-installed --verbose --without\"\n            ;;\n\n            (new)\n            opts=\"--backend --dist --help --license --name --no-git --non-interactive --overwrite --project-version --python --quiet --skip --verbose\"\n            ;;\n\n            (outdated)\n            opts=\"--global --help --include-sub --json --project --quiet --verbose\"\n            ;;\n\n            (plugin)\n            opts=\"--help --quiet --verbose\"\n            ;;\n\n            (publish)\n            opts=\"--ca-certs --comment --dest --help --identity --no-build --no-very-ssl --password --project --quiet --repository --sign --skip --skip-existing --username --verbose\"\n            ;;\n\n            (py)\n            opts=\"--help\"\n            ;;\n\n            (python)\n            opts=\"--help\"\n            ;;\n\n            (remove)\n            opts=\"--config-setting --dev --dry-run --fail-fast --frozen-lockfile --global --group --help --lockfile --no-editable --no-isolation --no-self --no-sync --override --project --quiet --skip --venv --verbose\"\n            ;;\n\n            (run)\n            opts=\"--global --help --json --list --project --quiet --recreate --site-packages --skip --venv --verbose\"\n            ;;\n\n            (search)\n            opts=\"--help --quiet --verbose\"\n            ;;\n\n            (self)\n            opts=\"--help --quiet --verbose\"\n            ;;\n\n            (show)\n            opts=\"--global --help --keywords --license --name --platform --project --quiet --summary --venv --verbose --version\"\n            ;;\n\n            (sync)\n            opts=\"--clean --clean-unselected --config-setting --dev --dry-run --fail-fast --global --group --help --lockfile --no-default --no-editable --no-isolation --no-self --production --project --quiet --reinstall --skip --venv --verbose --without\"\n            ;;\n\n            (update)\n            opts=\"--config-setting --dev --fail-fast --frozen-lockfile --global --group --help --lockfile --no-default --no-editable --no-isolation --no-self --no-sync --outdated --override --prerelease --production --project --quiet --save-compatible --save-exact --save-minimum --save-safe-compatible --save-wildcard --skip --stable --top --unconstrained --update-all --update-eager --update-reuse --update-reuse-installed --venv --verbose --without\"\n            ;;\n\n            (use)\n            opts=\"--auto-install-max --auto-install-min --first --global --help --ignore-remembered --no-version-file --project --quiet --skip --venv --verbose\"\n            ;;\n\n            (venv)\n            opts=\"--help --path --project --python\"\n            ;;\n\n        esac\n\n        COMPREPLY=($(compgen -W \"${opts}\" -- ${cur}))\n        __ltrim_colon_completions \"$cur\"\n\n        return 0;\n    fi\n\n    # completing for a command\n    if [[ $cur == $com ]]; then\n        coms=\"add build cache completion config export fix import info init install list lock new outdated plugin publish py python remove run search self show sync update use venv\"\n\n        COMPREPLY=($(compgen -W \"${coms}\" -- ${cur}))\n        __ltrim_colon_completions \"$cur\"\n\n        return 0\n    fi\n}\n\ncomplete -o default -F _pdm_a919b69078acdf0a_complete pdm\n"
  },
  {
    "path": "src/pdm/cli/completions/pdm.fish",
    "content": "# FISH completion script for pdm\n# Generated by pycomplete 0.4.0\n\nfunction __fish_pdm_a919b69078acdf0a_complete_no_subcommand\n    for i in (commandline -opc)\n        if contains -- $i add build cache completion config export fix import info init install list lock new outdated plugin publish py python remove run search self show sync update use venv\n            return 1\n        end\n    end\n    return 0\nend\n\n# global options\ncomplete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l config -d 'Specify another config file path [env var: PDM_CONFIG_FILE] '\ncomplete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l ignore-python -d 'Ignore the Python path saved in .pdm-python. [env var: PDM_IGNORE_SAVED_PYTHON]'\ncomplete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l no-cache -d 'Disable the cache for the current command. [env var: PDM_NO_CACHE]'\ncomplete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l non-interactive -d 'Don\\'t show interactive prompts but use defaults. [env var: PDM_NON_INTERACTIVE]'\ncomplete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l pep582 -d 'Print the command line to be eval\\'d by the shell for PEP 582'\ncomplete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l quiet -d 'Suppress output'\ncomplete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\ncomplete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l version -d 'Show the version and exit'\n\n# commands\n# add\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a add -d 'Add package(s) to pyproject.toml and install them'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l config-setting -d 'Pass options to the builder. options with a value must be specified after \"=\": `--config-setting=key(=value)` or `-Ckey(=value)`'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l dev -d 'Add packages into dev dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l dry-run -d 'Show the difference only and don\\'t perform any action'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l editable -d 'Specify editable packages'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l fail-fast -d 'Abort on first installation error'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l frozen-lockfile -d 'Don\\'t try to create or update the lockfile. [env var: PDM_FROZEN_LOCKFILE]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l global -d 'Use the global project, supply the project root with `-p` option'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l group -d 'Specify the target dependency group to add into'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l lockfile -d 'Specify another lockfile path. Default: pdm.lock. [env var: PDM_LOCKFILE]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l no-editable -d 'Install non-editable versions for all packages. [env var: PDM_NO_EDITABLE]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l no-isolation -d 'Disable isolation when building a source distribution that follows PEP 517, as in: build dependencies specified by PEP 518 must be already installed if this option is used.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l no-self -d 'Don\\'t install the project itself. [env var: PDM_NO_SELF]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l no-sync -d 'Only write pyproject.toml and do not sync the working set'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l override -d 'Use the constraint file in pip-requirements format for overriding. [env var: PDM_OVERRIDE] This option can be used multiple times. See https://pip.pypa.io/en/stable/user_guide/#constraints-files'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l prerelease -d 'Allow prereleases to be pinned'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l save-compatible -d 'Save compatible version specifiers'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l save-exact -d 'Save exact version specifiers'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l save-minimum -d 'Save minimum version specifiers'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l save-safe-compatible -d 'Save safe compatible version specifiers'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l save-wildcard -d 'Save wildcard version specifiers'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use \":all\" to skip all hooks. Use \":pre\" and \":post\" to skip all pre or post hooks.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l stable -d 'Only allow stable versions to be pinned'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l unconstrained -d 'Ignore the version constraints in pyproject.toml and overwrite with new ones from the resolution result'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l update-all -d 'Update all dependencies and sub-dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l update-eager -d 'Try to update the packages and their dependencies recursively'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l update-reuse -d 'Reuse pinned versions already present in lock file if possible'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l update-reuse-installed -d 'Reuse installed packages if possible'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_IN_VENV]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from add' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# build\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a build -d 'Build artifacts for distribution'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from build' -l config-setting -d 'Pass options to the builder. options with a value must be specified after \"=\": `--config-setting=key(=value)` or `-Ckey(=value)`'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from build' -l dest -d 'Target directory to put artifacts'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from build' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from build' -l no-clean -d 'Do not clean the target directory'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from build' -l no-isolation -d 'Disable isolation when building a source distribution that follows PEP 517, as in: build dependencies specified by PEP 518 must be already installed if this option is used.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from build' -l no-sdist -d 'Don\\'t build source tarballs'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from build' -l no-wheel -d 'Don\\'t build wheels'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from build' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from build' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from build' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use \":all\" to skip all hooks. Use \":pre\" and \":post\" to skip all pre or post hooks.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from build' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# cache\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a cache -d 'Control the caches of PDM'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from cache' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from cache' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from cache' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n# cache subcommands\nset -l cache_subcommands clear info list remove\n# cache clear\ncomplete -c pdm -f -n '__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from $cache_subcommands' -a clear -d 'Clean all the files under cache directory'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clear' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clear' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clear' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# cache info\ncomplete -c pdm -f -n '__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from $cache_subcommands' -a info -d 'Show the info and current size of caches'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from info' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from info' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from info' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# cache list\ncomplete -c pdm -f -n '__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from $cache_subcommands' -a list -d 'List the built wheels stored in the cache'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from list' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from list' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from list' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# cache remove\ncomplete -c pdm -f -n '__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from $cache_subcommands' -a remove -d 'Remove files matching the given pattern'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from remove' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from remove' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from remove' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# completion\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a completion -d 'Generate completion scripts for the given shell'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from completion' -l help -d 'Show this help message and exit.'\n\n# config\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a config -d 'Display the current configuration'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from config' -l delete -d 'Unset a configuration key'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from config' -l edit -d 'Edit the configuration file in the default editor(defined by EDITOR env var)'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from config' -l global -d 'Use the global project, supply the project root with `-p` option'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from config' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from config' -l local -d 'Set config in the project\\'s local configuration file'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from config' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from config' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from config' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# export\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a export -d 'Export the locked packages set to other formats'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l dev -d 'Select dev dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l editable-self -d 'Include the project itself as an editable dependency'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l expandvars -d 'Expand environment variables in requirements'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l format -d 'Export to requirements.txt format or pylock.toml format'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l global -d 'Use the global project, supply the project root with `-p` option'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l group -d 'Select group of optional-dependencies separated by comma or dependency-groups (with `-d`). Can be supplied multiple times, use \":all\" to include all groups under the same species.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l lockfile -d 'Specify another lockfile path. Default: pdm.lock. [env var: PDM_LOCKFILE]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l no-default -d 'Don\\'t include dependencies from the default group'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l no-extras -d 'Strip extras from the requirements'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l no-markers -d '(DEPRECATED)Don\\'t include platform markers'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l output -d 'Write output to the given file, or print to stdout if not given'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l production -d 'Unselect dev dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l pyproject -d 'Read the list of packages from pyproject.toml'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l self -d 'Include the project itself'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l without -d 'Exclude groups of optional-dependencies or dependency-groups'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from export' -l without-hashes -d 'Don\\'t include artifact hashes'\n\n# fix\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a fix -d 'Fix the project problems according to the latest version of PDM'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from fix' -l dry-run -d 'Only show the problems'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from fix' -l global -d 'Use the global project, supply the project root with `-p` option'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from fix' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from fix' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from fix' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from fix' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# import\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a import -d 'Import project metadata from other formats'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from import' -l dev -d 'import packages into dev dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from import' -l format -d 'Specify the file format explicitly'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from import' -l global -d 'Use the global project, supply the project root with `-p` option'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from import' -l group -d 'Specify the target dependency group to import into'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from import' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from import' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from import' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from import' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# info\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a info -d 'Show the project information'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from info' -l env -d 'Show PEP 508 environment markers'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from info' -l global -d 'Use the global project, supply the project root with `-p` option'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from info' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from info' -l json -d 'Dump the information in JSON'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from info' -l packages -d 'Show the local packages root'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from info' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from info' -l python -d 'Show the interpreter path'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from info' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from info' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_IN_VENV]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from info' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from info' -l where -d 'Show the project root path'\n\n# init\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a init -d 'Initialize a pyproject.toml for PDM.\n\n    Built-in templates:\n    - default: `pdm init`, A simple template with a basic structure.\n    - minimal: `pdm init minimal`, A minimal template with only `pyproject.toml`.\n    '\ncomplete -c pdm -A -n '__fish_seen_subcommand_from init' -l backend -d 'Specify the build backend, which implies --dist'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from init' -l cookiecutter -d 'Use Cookiecutter to generate project \u001b[32m[installed]\u001b[0m'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from init' -l copier -d 'Use Copier to generate project \u001b[32m[installed]\u001b[0m'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from init' -l dist -d 'Create a package for distribution'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from init' -l global -d 'Use the global project, supply the project root with `-p` option'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from init' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from init' -l license -d 'Specify the license (SPDX name)'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from init' -l name -d 'Specify the project name'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from init' -l no-git -d 'Do not initialize a git repository'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from init' -l non-interactive -d 'Don\\'t ask questions but use default values'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from init' -l overwrite -d 'Overwrite existing files'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from init' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from init' -l project-version -d 'Specify the project\\'s version'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from init' -l python -d 'Specify the Python version/path to use'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from init' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from init' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use \":all\" to skip all hooks. Use \":pre\" and \":post\" to skip all pre or post hooks.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from init' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# install\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a install -d 'Install dependencies from lock file'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l check -d 'Check if the lock file is up to date and fail otherwise'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l config-setting -d 'Pass options to the builder. options with a value must be specified after \"=\": `--config-setting=key(=value)` or `-Ckey(=value)`'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l dev -d 'Select dev dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l dry-run -d 'Show the difference only and don\\'t perform any action'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l fail-fast -d 'Abort on first installation error'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l frozen-lockfile -d 'Don\\'t try to create or update the lockfile. [env var: PDM_FROZEN_LOCKFILE]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l global -d 'Use the global project, supply the project root with `-p` option'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l group -d 'Select group of optional-dependencies separated by comma or dependency-groups (with `-d`). Can be supplied multiple times, use \":all\" to include all groups under the same species.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l lockfile -d 'Specify another lockfile path. Default: pdm.lock. [env var: PDM_LOCKFILE]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-default -d 'Don\\'t include dependencies from the default group'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-editable -d 'Install non-editable versions for all packages. [env var: PDM_NO_EDITABLE]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-isolation -d 'Disable isolation when building a source distribution that follows PEP 517, as in: build dependencies specified by PEP 518 must be already installed if this option is used.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-self -d 'Don\\'t install the project itself. [env var: PDM_NO_SELF]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l override -d 'Use the constraint file in pip-requirements format for overriding. [env var: PDM_OVERRIDE] This option can be used multiple times. See https://pip.pypa.io/en/stable/user_guide/#constraints-files'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l plugins -d 'Install the plugins specified in pyproject.toml'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l production -d 'Unselect dev dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use \":all\" to skip all hooks. Use \":pre\" and \":post\" to skip all pre or post hooks.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_IN_VENV]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from install' -l without -d 'Exclude groups of optional-dependencies or dependency-groups'\n\n# list\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a list -d 'List packages installed in the current working set'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from list' -l csv -d 'Output dependencies in CSV document format'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from list' -l exclude -d 'Exclude dependency groups from the output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from list' -l fields -d 'Select information to output as a comma separated string. All fields: groups,homepage,licenses,location,name,version.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from list' -l freeze -d 'Show the installed dependencies in pip\\'s requirements.txt format'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from list' -l global -d 'Use the global project, supply the project root with `-p` option'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from list' -l graph -d 'Display a tree of dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from list' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from list' -l include -d 'Dependency groups to include in the output. By default all are included'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from list' -l json -d 'Output dependencies in JSON document format'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from list' -l markdown -d 'Output dependencies and legal notices in markdown document format - best effort basis'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from list' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from list' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from list' -l resolve -d 'Resolve all requirements to output licenses (instead of just showing those currently installed)'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from list' -l reverse -d 'Reverse the dependency tree'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from list' -l sort -d 'Sort the output using a given field name. If nothing is set, no sort is applied. Multiple fields can be combined with \\',\\'.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from list' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_IN_VENV]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from list' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# lock\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a lock -d 'Resolve and lock dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l append -d 'Append the result to the current lock file'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l check -d 'Check if the lock file is up to date and quit'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l config-setting -d 'Pass options to the builder. options with a value must be specified after \"=\": `--config-setting=key(=value)` or `-Ckey(=value)`'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l dev -d 'Select dev dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l exclude-newer -d 'Exclude packages newer than the given UTC date in format `YYYY-MM-DD[THH:MM:SSZ]`'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l global -d 'Use the global project, supply the project root with `-p` option'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l group -d 'Select group of optional-dependencies separated by comma or dependency-groups (with `-d`). Can be supplied multiple times, use \":all\" to include all groups under the same species.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l implementation -d 'The Python implementation to lock for. E.g. `cpython`, `pypy`, `pyston`'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l lockfile -d 'Specify another lockfile path. Default: pdm.lock. [env var: PDM_LOCKFILE]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l no-cross-platform -d '[DEPRECATED] Only lock packages for the current platform'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l no-default -d 'Don\\'t include dependencies from the default group'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l no-isolation -d 'Disable isolation when building a source distribution that follows PEP 517, as in: build dependencies specified by PEP 518 must be already installed if this option is used.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l no-static-urls -d '[DEPRECATED] Do not store static file URLs in the lockfile'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l override -d 'Use the constraint file in pip-requirements format for overriding. [env var: PDM_OVERRIDE] This option can be used multiple times. See https://pip.pypa.io/en/stable/user_guide/#constraints-files'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l platform -d 'The platform to lock for. E.g. `windows`, `linux`, `macos`, `manylinux_2_17_x86_64`. See docs for available choices: http://pdm-project.org/en/latest/usage/lock-targets/'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l production -d 'Unselect dev dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l python -d 'The Python range to lock for. E.g. `>=3.9`, `==3.12.*`'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l refresh -d 'Refresh the content hash and file hashes in the lock file'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use \":all\" to skip all hooks. Use \":pre\" and \":post\" to skip all pre or post hooks.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l static-urls -d '[DEPRECATED] Store static file URLs in the lockfile'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l strategy -d 'Specify lock strategy (cross_platform, static_urls, direct_minimal_versions, inherit_metadata). Add \\'no_\\' prefix to disable. Can be supplied multiple times or split by comma.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l update-reuse -d 'Reuse pinned versions already present in lock file if possible'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l update-reuse-installed -d 'Reuse installed packages if possible'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from lock' -l without -d 'Exclude groups of optional-dependencies or dependency-groups'\n\n# new\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a new -d 'Create a new Python project at <project_path>'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from new' -l backend -d 'Specify the build backend, which implies --dist'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from new' -l dist -d 'Create a package for distribution'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from new' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from new' -l license -d 'Specify the license (SPDX name)'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from new' -l name -d 'Specify the project name'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from new' -l no-git -d 'Do not initialize a git repository'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from new' -l non-interactive -d 'Don\\'t ask questions but use default values'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from new' -l overwrite -d 'Overwrite existing files'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from new' -l project-version -d 'Specify the project\\'s version'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from new' -l python -d 'Specify the Python version/path to use'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from new' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from new' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use \":all\" to skip all hooks. Use \":pre\" and \":post\" to skip all pre or post hooks.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from new' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# outdated\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a outdated -d 'Check for outdated packages and list the latest versions on indexes.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from outdated' -l global -d 'Use the global project, supply the project root with `-p` option'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from outdated' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from outdated' -l include-sub -d 'Include sub-dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from outdated' -l json -d 'Output in JSON format'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from outdated' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from outdated' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from outdated' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# plugin\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a plugin -d 'Manage the PDM program itself (previously known as plugin)'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n# plugin subcommands\nset -l plugin_subcommands add list remove update\n# plugin add\ncomplete -c pdm -f -n '__fish_seen_subcommand_from plugin; and not __fish_seen_subcommand_from $plugin_subcommands' -a add -d 'Install packages to the PDM\\'s environment'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from add' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from add' -l pip-args -d 'Arguments that will be passed to pip install'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from add' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from add' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# plugin list\ncomplete -c pdm -f -n '__fish_seen_subcommand_from plugin; and not __fish_seen_subcommand_from $plugin_subcommands' -a list -d 'List all packages installed with PDM'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from list' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from list' -l plugins -d 'List plugins only'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from list' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from list' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# plugin remove\ncomplete -c pdm -f -n '__fish_seen_subcommand_from plugin; and not __fish_seen_subcommand_from $plugin_subcommands' -a remove -d 'Remove packages from PDM\\'s environment'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from remove' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from remove' -l pip-args -d 'Arguments that will be passed to pip uninstall'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from remove' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from remove' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from remove' -l yes -d 'Answer yes on the question'\n\n# plugin update\ncomplete -c pdm -f -n '__fish_seen_subcommand_from plugin; and not __fish_seen_subcommand_from $plugin_subcommands' -a update -d 'Update PDM itself'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from update' -l head -d 'Update to the latest commit on the main branch'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from update' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from update' -l no-frozen-deps -d 'Do not install frozen dependency versions'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from update' -l pip-args -d 'Additional arguments that will be passed to pip install'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from update' -l pre -d 'Update to the latest prerelease version'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from update' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from update' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# publish\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a publish -d 'Build and publish the project to PyPI'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from publish' -l ca-certs -d 'The path to a PEM-encoded Certificate Authority bundle to use for publish server validation [env var: PDM_PUBLISH_CA_CERTS]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from publish' -l comment -d 'The comment to include with the distribution file.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from publish' -l dest -d 'The directory to upload the package from'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from publish' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from publish' -l identity -d 'GPG identity used to sign files.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from publish' -l no-build -d 'Don\\'t build the package before publishing'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from publish' -l no-very-ssl -d 'Disable SSL verification'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from publish' -l password -d 'The password to access the repository [env var: PDM_PUBLISH_PASSWORD]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from publish' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from publish' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from publish' -l repository -d 'The repository name or url to publish the package to [env var: PDM_PUBLISH_REPO]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from publish' -l sign -d 'Upload the package with PGP signature'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from publish' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use \":all\" to skip all hooks. Use \":pre\" and \":post\" to skip all pre or post hooks.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from publish' -l skip-existing -d 'Skip uploading files that already exist. This may not work with some repository implementations.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from publish' -l username -d 'The username to access the repository [env var: PDM_PUBLISH_USERNAME]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from publish' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# py\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a py -d 'Manage installed Python interpreters'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py' -l help -d 'Show this help message and exit.'\n# py subcommands\nset -l py_subcommands find install link list remove\n# py find\ncomplete -c pdm -f -n '__fish_seen_subcommand_from py; and not __fish_seen_subcommand_from $py_subcommands' -a find -d 'Search for a Python interpreter'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from find' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from find' -l managed -d 'Only find interpreters managed by PDM'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from find' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from find' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# py install\ncomplete -c pdm -f -n '__fish_seen_subcommand_from py; and not __fish_seen_subcommand_from $py_subcommands' -a install -d 'Install a Python interpreter with PDM'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from install' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from install' -l list -d 'List all available Python versions'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from install' -l min -d 'Use minimum instead of highest version for installation if `version` is left empty'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from install' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from install' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# py link\ncomplete -c pdm -f -n '__fish_seen_subcommand_from py; and not __fish_seen_subcommand_from $py_subcommands' -a link -d 'Link an external Python interpreter to PDM'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from link' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from link' -l name -d 'The name of the link'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from link' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from link' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# py list\ncomplete -c pdm -f -n '__fish_seen_subcommand_from py; and not __fish_seen_subcommand_from $py_subcommands' -a list -d 'List all Python interpreters installed with PDM'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from list' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from list' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from list' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# py remove\ncomplete -c pdm -f -n '__fish_seen_subcommand_from py; and not __fish_seen_subcommand_from $py_subcommands' -a remove -d 'Remove a Python interpreter installed with PDM'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from remove' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from remove' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from py; and __fish_seen_subcommand_from remove' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# python\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a python -d 'Manage installed Python interpreters'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python' -l help -d 'Show this help message and exit.'\n# python subcommands\nset -l python_subcommands find install link list remove\n# python find\ncomplete -c pdm -f -n '__fish_seen_subcommand_from python; and not __fish_seen_subcommand_from $python_subcommands' -a find -d 'Search for a Python interpreter'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from find' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from find' -l managed -d 'Only find interpreters managed by PDM'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from find' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from find' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# python install\ncomplete -c pdm -f -n '__fish_seen_subcommand_from python; and not __fish_seen_subcommand_from $python_subcommands' -a install -d 'Install a Python interpreter with PDM'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from install' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from install' -l list -d 'List all available Python versions'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from install' -l min -d 'Use minimum instead of highest version for installation if `version` is left empty'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from install' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from install' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# python link\ncomplete -c pdm -f -n '__fish_seen_subcommand_from python; and not __fish_seen_subcommand_from $python_subcommands' -a link -d 'Link an external Python interpreter to PDM'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from link' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from link' -l name -d 'The name of the link'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from link' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from link' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# python list\ncomplete -c pdm -f -n '__fish_seen_subcommand_from python; and not __fish_seen_subcommand_from $python_subcommands' -a list -d 'List all Python interpreters installed with PDM'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from list' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from list' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from list' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# python remove\ncomplete -c pdm -f -n '__fish_seen_subcommand_from python; and not __fish_seen_subcommand_from $python_subcommands' -a remove -d 'Remove a Python interpreter installed with PDM'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from remove' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from remove' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from python; and __fish_seen_subcommand_from remove' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# remove\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a remove -d 'Remove packages from pyproject.toml'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l config-setting -d 'Pass options to the builder. options with a value must be specified after \"=\": `--config-setting=key(=value)` or `-Ckey(=value)`'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l dev -d 'Remove packages from dev dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l dry-run -d 'Show the difference only and don\\'t perform any action'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l fail-fast -d 'Abort on first installation error'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l frozen-lockfile -d 'Don\\'t try to create or update the lockfile. [env var: PDM_FROZEN_LOCKFILE]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l global -d 'Use the global project, supply the project root with `-p` option'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l group -d 'Specify the target dependency group to remove from'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l lockfile -d 'Specify another lockfile path. Default: pdm.lock. [env var: PDM_LOCKFILE]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l no-editable -d 'Install non-editable versions for all packages. [env var: PDM_NO_EDITABLE]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l no-isolation -d 'Disable isolation when building a source distribution that follows PEP 517, as in: build dependencies specified by PEP 518 must be already installed if this option is used.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l no-self -d 'Don\\'t install the project itself. [env var: PDM_NO_SELF]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l no-sync -d 'Only write pyproject.toml and do not uninstall packages'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l override -d 'Use the constraint file in pip-requirements format for overriding. [env var: PDM_OVERRIDE] This option can be used multiple times. See https://pip.pypa.io/en/stable/user_guide/#constraints-files'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use \":all\" to skip all hooks. Use \":pre\" and \":post\" to skip all pre or post hooks.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_IN_VENV]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from remove' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# run\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a run -d 'Run commands or scripts with local packages loaded'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from run' -l global -d 'Use the global project, supply the project root with `-p` option'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from run' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from run' -l json -d 'Output all scripts infos in JSON'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from run' -l list -d 'Show all available scripts defined in pyproject.toml'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from run' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from run' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from run' -l recreate -d 'Recreate the script environment for self-contained scripts'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from run' -l site-packages -d 'Load site-packages from the selected interpreter'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from run' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use \":all\" to skip all hooks. Use \":pre\" and \":post\" to skip all pre or post hooks.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from run' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_IN_VENV]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from run' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# search\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a search -d 'Search for PyPI packages'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from search' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from search' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from search' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# self\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a self -d 'Manage the PDM program itself (previously known as plugin)'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n# self subcommands\nset -l self_subcommands add list remove update\n# self add\ncomplete -c pdm -f -n '__fish_seen_subcommand_from self; and not __fish_seen_subcommand_from $self_subcommands' -a add -d 'Install packages to the PDM\\'s environment'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from add' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from add' -l pip-args -d 'Arguments that will be passed to pip install'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from add' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from add' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# self list\ncomplete -c pdm -f -n '__fish_seen_subcommand_from self; and not __fish_seen_subcommand_from $self_subcommands' -a list -d 'List all packages installed with PDM'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from list' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from list' -l plugins -d 'List plugins only'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from list' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from list' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# self remove\ncomplete -c pdm -f -n '__fish_seen_subcommand_from self; and not __fish_seen_subcommand_from $self_subcommands' -a remove -d 'Remove packages from PDM\\'s environment'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from remove' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from remove' -l pip-args -d 'Arguments that will be passed to pip uninstall'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from remove' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from remove' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from remove' -l yes -d 'Answer yes on the question'\n\n# self update\ncomplete -c pdm -f -n '__fish_seen_subcommand_from self; and not __fish_seen_subcommand_from $self_subcommands' -a update -d 'Update PDM itself'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from update' -l head -d 'Update to the latest commit on the main branch'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from update' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from update' -l no-frozen-deps -d 'Do not install frozen dependency versions'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from update' -l pip-args -d 'Additional arguments that will be passed to pip install'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from update' -l pre -d 'Update to the latest prerelease version'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from update' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from self; and __fish_seen_subcommand_from update' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# show\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a show -d 'Show the package information'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from show' -l global -d 'Use the global project, supply the project root with `-p` option'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from show' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from show' -l keywords -d 'Show keywords'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from show' -l license -d 'Show license'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from show' -l name -d 'Show name'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from show' -l platform -d 'Show platform'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from show' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from show' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from show' -l summary -d 'Show summary'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from show' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_IN_VENV]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from show' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from show' -l version -d 'Show version'\n\n# sync\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a sync -d 'Synchronize the current working set with lock file'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l clean -d 'Clean packages not in the lockfile'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l clean-unselected -d 'Only keep the selected packages'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l config-setting -d 'Pass options to the builder. options with a value must be specified after \"=\": `--config-setting=key(=value)` or `-Ckey(=value)`'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l dev -d 'Select dev dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l dry-run -d 'Show the difference only and don\\'t perform any action'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l fail-fast -d 'Abort on first installation error'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l global -d 'Use the global project, supply the project root with `-p` option'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l group -d 'Select group of optional-dependencies separated by comma or dependency-groups (with `-d`). Can be supplied multiple times, use \":all\" to include all groups under the same species.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l lockfile -d 'Specify another lockfile path. Default: pdm.lock. [env var: PDM_LOCKFILE]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l no-default -d 'Don\\'t include dependencies from the default group'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l no-editable -d 'Install non-editable versions for all packages. [env var: PDM_NO_EDITABLE]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l no-isolation -d 'Disable isolation when building a source distribution that follows PEP 517, as in: build dependencies specified by PEP 518 must be already installed if this option is used.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l no-self -d 'Don\\'t install the project itself. [env var: PDM_NO_SELF]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l production -d 'Unselect dev dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l reinstall -d 'Force reinstall existing dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use \":all\" to skip all hooks. Use \":pre\" and \":post\" to skip all pre or post hooks.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_IN_VENV]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from sync' -l without -d 'Exclude groups of optional-dependencies or dependency-groups'\n\n# update\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a update -d 'Update package(s) in pyproject.toml'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l config-setting -d 'Pass options to the builder. options with a value must be specified after \"=\": `--config-setting=key(=value)` or `-Ckey(=value)`'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l dev -d 'Select dev dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l fail-fast -d 'Abort on first installation error'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l frozen-lockfile -d 'Don\\'t try to create or update the lockfile. [env var: PDM_FROZEN_LOCKFILE]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l global -d 'Use the global project, supply the project root with `-p` option'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l group -d 'Select group of optional-dependencies separated by comma or dependency-groups (with `-d`). Can be supplied multiple times, use \":all\" to include all groups under the same species.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l lockfile -d 'Specify another lockfile path. Default: pdm.lock. [env var: PDM_LOCKFILE]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l no-default -d 'Don\\'t include dependencies from the default group'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l no-editable -d 'Install non-editable versions for all packages. [env var: PDM_NO_EDITABLE]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l no-isolation -d 'Disable isolation when building a source distribution that follows PEP 517, as in: build dependencies specified by PEP 518 must be already installed if this option is used.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l no-self -d 'Don\\'t install the project itself. [env var: PDM_NO_SELF]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l no-sync -d 'Only update lock file but do not sync packages'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l outdated -d 'Show the difference only without modifying the lockfile content'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l override -d 'Use the constraint file in pip-requirements format for overriding. [env var: PDM_OVERRIDE] This option can be used multiple times. See https://pip.pypa.io/en/stable/user_guide/#constraints-files'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l prerelease -d 'Allow prereleases to be pinned'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l production -d 'Unselect dev dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l save-compatible -d 'Save compatible version specifiers'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l save-exact -d 'Save exact version specifiers'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l save-minimum -d 'Save minimum version specifiers'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l save-safe-compatible -d 'Save safe compatible version specifiers'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l save-wildcard -d 'Save wildcard version specifiers'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use \":all\" to skip all hooks. Use \":pre\" and \":post\" to skip all pre or post hooks.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l stable -d 'Only allow stable versions to be pinned'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l top -d 'Only update those listed in pyproject.toml'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l unconstrained -d 'Ignore the version constraints in pyproject.toml and overwrite with new ones from the resolution result'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l update-all -d 'Update all dependencies and sub-dependencies'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l update-eager -d 'Try to update the packages and their dependencies recursively'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l update-reuse -d 'Reuse pinned versions already present in lock file if possible'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l update-reuse-installed -d 'Reuse installed packages if possible'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_IN_VENV]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from update' -l without -d 'Exclude groups of optional-dependencies or dependency-groups'\n\n# use\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a use -d 'Use the given python version or path as base interpreter. If not found, PDM will try to install one.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from use' -l auto-install-max -d 'If `python` argument not given, auto install maximum best match - otherwise has no effect'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from use' -l auto-install-min -d 'If `python` argument not given, auto install minimal best match - otherwise has no effect'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from use' -l first -d 'Select the first matched interpreter - no auto install'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from use' -l global -d 'Use the global project, supply the project root with `-p` option'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from use' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from use' -l ignore-remembered -d 'Ignore the remembered selection'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from use' -l no-version-file -d 'Do not write .python-version file'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from use' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from use' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from use' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use \":all\" to skip all hooks. Use \":pre\" and \":post\" to skip all pre or post hooks.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from use' -l venv -d 'Use the interpreter in the virtual environment with the given name'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from use' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# venv\ncomplete -c pdm -f -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -a venv -d 'Virtualenv management'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv' -l path -d 'Show the path to the given virtualenv'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv' -l python -d 'Show the python interpreter path for the given virtualenv'\n# venv subcommands\nset -l venv_subcommands activate create list purge remove\n# venv activate\ncomplete -c pdm -f -n '__fish_seen_subcommand_from venv; and not __fish_seen_subcommand_from $venv_subcommands' -a activate -d 'Print the command to activate the virtualenv with the given name'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from activate' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from activate' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from activate' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# venv create\ncomplete -c pdm -f -n '__fish_seen_subcommand_from venv; and not __fish_seen_subcommand_from $venv_subcommands' -a create -d 'Create a virtualenv'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from create' -l force -d 'Recreate if the virtualenv already exists'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from create' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from create' -l name -d 'Specify the name of the virtualenv'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from create' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from create' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from create' -l with -d 'Specify the backend to create the virtualenv'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from create' -l with-pip -d 'Install pip with the virtualenv'\n\n# venv list\ncomplete -c pdm -f -n '__fish_seen_subcommand_from venv; and not __fish_seen_subcommand_from $venv_subcommands' -a list -d 'List all virtualenvs associated with this project'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from list' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from list' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from list' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# venv purge\ncomplete -c pdm -f -n '__fish_seen_subcommand_from venv; and not __fish_seen_subcommand_from $venv_subcommands' -a purge -d 'Purge selected/all created Virtualenvs'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from purge' -l force -d 'Force purging without prompting for confirmation'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from purge' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from purge' -l interactive -d 'Interactively purge selected Virtualenvs'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from purge' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from purge' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\n\n# venv remove\ncomplete -c pdm -f -n '__fish_seen_subcommand_from venv; and not __fish_seen_subcommand_from $venv_subcommands' -a remove -d 'Remove the virtualenv with the given name'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from remove' -l help -d 'Show this help message and exit.'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from remove' -l quiet -d 'Suppress output'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from remove' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'\ncomplete -c pdm -A -n '__fish_seen_subcommand_from venv; and __fish_seen_subcommand_from remove' -l yes -d 'Answer yes on the following question'\n"
  },
  {
    "path": "src/pdm/cli/completions/pdm.ps1",
    "content": "# Powershell completion script for pdm\n\nif ((Test-Path Function:\\TabExpansion) -and -not (Test-Path Function:\\_pdm_completeBackup)) {\n    Rename-Item Function:\\TabExpansion _pdm_completeBackup\n}\n\n$PDM_PYTHON = \"%{python_executable}\"\n$PDM_PIP_INDEX = (& $PDM_PYTHON -m pdm config pypi.url).Trim()\n$CONFIG_DIR = \"$env:LOCALAPPDATA\\pdm\"\n\nclass Option {\n    [string[]] $Opts\n    [string[]] $Values\n\n    Option([string[]] $opts) {\n        $this.Opts = $opts\n    }\n\n    [Option] WithValues([string[]] $values) {\n        $this.Values = $values\n        return $this\n    }\n\n    [bool] Match([string] $word) {\n        foreach ($opt in $this.Opts) {\n            if ($word -eq $opt) {\n                return $true\n            }\n        }\n        return $false\n    }\n\n    [bool] TakesArg() {\n        return $null -ne $this.Values\n    }\n}\n\nclass Completer {\n\n    [string []] $params\n    [bool] $multiple = $false\n    [Option[]] $opts = @()\n\n    Completer() {\n    }\n\n    [string[]] Complete([string[]] $words) {\n        $expectArg = $null\n        $lastWord = $words[-1]\n        $paramUsed = $false\n        if ($words.Length -gt 1) {\n            foreach ($word in $words[0..($words.Length - 2)]) {\n                if ($expectArg) {\n                    $expectArg = $null\n                    continue\n                }\n                if ($word.StartsWith(\"-\")) {\n                    $opt = $this.opts.Where( { $_.Match($word) })[0]\n                    if ($null -ne $opt -and $opt.TakesArg()) {\n                        $expectArg = $opt\n                    }\n                }\n                elseif (-not $this.multiple) {\n                    $paramUsed = $true\n                }\n            }\n        }\n        $candidates = @()\n        if ($lastWord.StartsWith(\"-\")) {\n            foreach ($opt in $this.opts) {\n                $candidates += $opt.Opts\n            }\n        }\n        elseif ($null -ne $expectArg) {\n            $candidates = $expectArg.Values\n        }\n        elseif ($null -ne $this.params -and -not $paramUsed) {\n            $candidates = $this.params\n        }\n        return $candidates.Where( { $_.StartsWith($lastWord) })\n    }\n\n    [void] AddOpts([Option[]] $options) {\n        $this.opts += $options\n    }\n\n    [void] AddParams([string[]] $params, [bool]$multiple = $false) {\n        $this.params = $params\n        $this.multiple = $multiple\n    }\n}\n\nfunction getSections() {\n    if (-not (Test-Path -Path \"pyproject.toml\")) {\n        return @()\n    }\n    [string[]] $sections = @()\n    [bool] $inSection = $false\n    foreach ($line in (Get-Content \"pyproject.toml\")) {\n        if (($line -match ' *\\[project\\.optional-dependencies\\]') -or ($line -match ' *\\[tool\\.pdm.dev-dependencies\\]')) {\n            $inSection = $true\n        }\n        elseif ($inSection -and ($line -match '(\\S+) *= *\\[')) {\n            $sections += $Matches[1]\n        }\n        elseif ($line -like '`[*`]') {\n            $inSection = $false\n        }\n    }\n    return $sections\n}\n\nfunction _fetchPackageListFromPyPI() {\n    if (-not (Test-Path -Path $CONFIG_DIR)) {\n        mkdir $CONFIG_DIR\n    }\n    (Invoke-WebRequest $PDM_PIP_INDEX).Links | ForEach-Object { $_.innerText } | Out-File -FilePath \"$CONFIG_DIR\\.pypiPackages\"\n}\n\nfunction getPyPIPackages() {\n    # $cacheFile = \"$CONFIG_DIR\\.pypiPackages\"\n    # if (-not (Test-Path -Path $cacheFile) -or (Get-Item $cacheFile).LastWriteTime -lt (Get-Date).AddDays(-28)) {\n    #     _fetchPackageListFromPyPI\n    # }\n    # Get-Content $cacheFile\n}\n\nfunction getPdmPackages() {\n    & $PDM_PYTHON -c \"\nimport sys\nif sys.version_info >= (3, 11):\n  import tomllib\nelse:\n  import tomli as tomllib\nimport os, re\nPACKAGE_REGEX = re.compile(r'^[A-Za-z][A-Za-z0-9._-]*')\ndef get_packages(lines):\n    return [PACKAGE_REGEX.match(line).group() for line in lines]\n\nwith open('pyproject.toml', 'rb') as f:\n    data = tomllib.load(f)\npackages = get_packages(data.get('project', {}).get('dependencies', []))\nfor reqs in data.get('project', {}).get('optional-dependencies', {}).values():\n    packages.extend(get_packages(reqs))\nfor reqs in data.get('tool', {}).get('pdm', {}).get('dev-dependencies', {}).values():\n    packages.extend(get_packages(reqs))\nprint(*set(packages), sep='\\n')\n\"\n}\n\n$_cachedConfigKeys = $null\nfunction getConfigKeys() {\n    if ($null -eq $_cachedConfigKeys) {\n        [string[]] $keys = @()\n        $config = @(& $PDM_PYTHON -m pdm config)\n        foreach ($line in $config) {\n            if ($line -match ' *(\\S+) *=') {\n                $keys += $Matches[1]\n            }\n        }\n        $_cachedConfigKeys = $keys\n    }\n    return $_cachedConfigKeys\n}\n\nfunction getScripts() {\n    [string[]] $scripts = @()\n    $packagesDir = (& $PDM_PYTHON -m pdm info --packages)\n    if (Test-Path -Path \"pyproject.toml\") {\n        [bool] $inScripts = $false\n        foreach ($line in (Get-Content \"pyproject.toml\")) {\n            if ($line -match ' *\\[tool\\.pdm\\.scripts\\]') {\n                $inScripts = $true\n            }\n            elseif ($inScripts -and ($line -match '(\\S+) *= *')) {\n                $scripts += $Matches[1]\n            }\n            elseif ($line -like '`[*`]') {\n                $inScripts = $false\n            }\n        }\n    }\n    if ($packagesDir -ne \"None\") {\n        $scripts += (Get-ChildItem \"$packagesDir\\Scripts\" | ForEach-Object { $_.Basename })\n    }\n    return $scripts\n\n}\n\nfunction TabExpansion($line, $lastWord) {\n    $lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()\n\n    if ($lastBlock -match \"^pdm \") {\n        [string[]]$words = $lastBlock.Split()[1..$lastBlock.Length]\n        [string[]]$AllCommands = (\n            \"add\", \"build\", \"cache\", \"completion\", \"config\", \"export\", \"fix\", \"import\", \"info\", \"init\", \"install\",\n            \"list\", \"lock\", \"outdated\", \"plugin\", \"publish\", \"remove\", \"run\", \"search\", \"show\", \"sync\", \"update\",\n            \"use\", \"python\", \"py\"\n        )\n        [string[]]$commands = $words.Where( { $_ -notlike \"-*\" })\n        $command = $commands[0]\n        $completer = [Completer]::new()\n        $completer.AddOpts(([Option]::new((\"-h\", \"--help\", \"-v\", \"--verbose\", \"-q\", \"--quiet\"))))\n        $sectionOption = [Option]::new(@(\"-G\", \"--group\", \"--with\", \"--without\")).WithValues(@(getSections))\n        $projectOption = [Option]::new(@(\"-p\", \"--project\")).WithValues(@())\n        $skipOption = [Option]::new(@(\"-k\", \"--skip\")).WithValues(@())\n        $venvOption = [Option]::new(@(\"--venv\")).WithValues(@())\n        $formatOption = [Option]::new(@(\"-f\", \"--format\")).WithValues(@(\"setuppy\", \"requirements\", \"poetry\", \"flit\"))\n\n        Switch ($command) {\n\n            \"add\" {\n                $completer.AddOpts(@(\n                        [Option]::new((\n                            \"-d\", \"--dev\", \"--save-compatible\", \"--save-wildcard\", \"--dry-run\", \"--save-exact\", \"--override\",\n                            \"--save-minimum\", \"--save-safe-compatible\", \"--update-eager\", \"--update-reuse\", \"--update-all\", \"-g\", \"--global\",\n                            \"--no-sync\", \"--no-editable\", \"--no-self\", \"-u\", \"--unconstrained\", \"--no-isolation\", \"-C\", \"--config-setting\", \"--stable\",\n                            \"--pre\", \"--prerelease\", \"-L\", \"--lockfile\", \"--fail-fast\", \"-x\", \"--frozen-lockfile\", \"--update-reuse-installed\"\n                        )),\n                        $sectionOption,\n                        $projectOption,\n                        $venvOption,\n                        $skipOption\n                        [Option]::new(@(\"-e\", \"--editable\")).WithValues(@(getPyPIPackages))\n                    ))\n                $completer.AddParams(@(getPyPIPackages), $true)\n                break\n            }\n            \"build\" { $completer.AddOpts(@([Option]::new(@(\"-d\", \"--dest\", \"--no-clean\", \"--no-sdist\", \"--no-wheel\", \"-C\", \"--config-setting\", \"--no-isolation\")), $projectOption, $skipOption)) }\n            \"cache\" {\n                $subCommand = $commands[1]\n                switch ($subCommand) {\n                    \"clear\" {\n                        $completer.AddParams(@(\"wheels\", \"http\", \"hashes\", \"metadata\"), $false)\n                        $command = $subCommand\n                    }\n                    \"remove\" {$command = $subCommand}\n                    \"info\" {$command = $subCommand}\n                    \"list\" {$command = $subCommand}\n                    default {\n                        $completer.AddParams(@(\"clear\", \"remove\", \"info\", \"list\"), $false)\n                    }\n                }\n                break\n            }\n            \"completion\" { $completer.AddParams(@(\"powershell\", \"bash\", \"zsh\", \"fish\")); break }\n            \"config\" {\n                $completer.AddOpts(@([Option]::new(@(\"--delete\", \"--global\", \"--local\", \"--edit\", \"-e\", \"-d\", \"-l\", \"-g\")), $projectOption))\n                $completer.AddParams(@(getConfigKeys), $false)\n                break\n            }\n            \"export\" {\n                $completer.AddOpts(@(\n                        [Option]::new(@(\n                            \"--dev\", \"--output\", \"--global\", \"--no-default\", \"--expandvars\", \"--prod\", \"--production\", \"-g\", \"-d\", \"-o\",\n                            \"--no-hashes\", \"--no-markers\", \"-L\", \"--lockfile\", \"--self\", \"--editable-self\", \"--no-extras\"\n                        )),\n                        $formatOption,\n                        $sectionOption,\n                        $projectOption\n                    ))\n                break\n            }\n            \"fix\" {\n                $completer.AddOpts(@(\n                        [Option]::new(@(\"--dry-run\", \"-g\", \"--global\")),\n                        $projectOption\n                    ))\n                break\n            }\n            \"import\" {\n                $completer.AddOpts(@(\n                        [Option]::new(@(\"--dev\", \"--global\", \"--no-default\", \"-g\", \"-d\")),\n                        $formatOption,\n                        $sectionOption,\n                        $projectOption\n                    ))\n                break\n            }\n            \"info\" {\n                $completer.AddOpts(\n                    @(\n                        [Option]::new(@(\"--env\", \"--global\", \"-g\", \"--python\", \"--where\", \"--packages\", \"--json\")),\n                        $projectOption,\n                        $venvOption\n                    ))\n                break\n            }\n            \"new\" {}\n            \"init\" {\n                $completer.AddOpts(\n                    @(\n                        [Option]::new(@(\n                            \"-g\", \"--global\", \"--non-interactive\", \"-n\", \"--python\", \"--dist\", \"--lib\", \"--copier\",\n                            \"--cookiecutter\", \"--overwrite\", \"--license\", \"--project-version\", \"--name\", \"--no-git\"\n                        )),\n                        $projectOption,\n                        $skipOption,\n                        [Option]::new(@(\"--backend\")).WithValues(@(\"pdm-backend\", \"setuptools\", \"flit\", \"hatching\"))\n                    ))\n                break\n            }\n            \"install\" {\n                $completer.AddOpts(@(\n                        [Option]::new((\n                            \"-d\", \"--dev\", \"-g\", \"--global\", \"--dry-run\", \"--no-default\", \"--frozen-lockfile\", \"--prod\",\n                            \"--production\", \"--no-editable\", \"--no-self\", \"-C\", \"--config-setting\", \"--no-isolation\", \"--check\", \"-L\",\n                            \"--lockfile\", \"--fail-fast\", \"-x\", \"--plugins\", \"--override\"\n                        )),\n                        $sectionOption,\n                        $skipOption,\n                        $venvOption,\n                        $projectOption\n                    ))\n                break\n            }\n            \"list\" {\n                $completer.AddOpts(\n                    @(\n                        [Option]::new(@(\n                            \"--graph\", \"--tree\", \"--global\", \"-g\", \"--reverse\", \"-r\", \"--freeze\",\"--json\", \"--csv\",\n                            \"--markdown\", \"--fields\", \"--sort\", \"--include\", \"--exclude\", \"--resolve\"\n                        )),\n                        $venvOption,\n                        $projectOption\n                    ))\n                break\n            }\n            \"lock\" {\n                $completer.AddOpts(\n                    @(\n                        [Option]::new(@(\n                            \"--global\", \"-g\", \"-C\", \"--config-setting\", \"--no-isolation\", \"--refresh\", \"-L\", \"--lockfile\", \"--check\", \"--dev\", \"--prod\",\n                            \"--production\", \"-d\", \"--no-default\", \"--no-cross-platform\", \"--static-urls\", \"--no-static-urls\", \"--override\",\n                            \"--strategy\", \"-S\", \"--update-reuse\", \"--update-reuse-installed\", \"--exclude-newer\", \"--append\",\n                            \"--platform\", \"--python\", \"--implementation\"\n                        )),\n                        $skipOption,\n                        $sectionOption,\n                        $projectOption\n                    ))\n                break\n            }\n            \"outdated\" {\n                $completer.AddOpts(\n                    @(\n                        [Option]::new(@(\"--json\")),\n                        $projectOption\n                    ))\n                break\n            }\n            \"self\" {\n                $subCommand = $commands[1]\n                switch ($subCommand) {\n                    \"add\" {\n                        $completer.AddOpts(([Option]::new((\"--pip-args\"))))\n                        $completer.AddParams(@(getPyPIPackages), $true)\n                        $command = $subCommand\n                        break\n                    }\n                    \"remove\" {\n                        $completer.AddOpts(([Option]::new((\"--pip-args\", \"-y\", \"--yes\"))))\n                        $command = $subCommand\n                        break\n                    }\n                    \"list\" {\n                        $completer.AddOpts(([Option]::new((\"--plugins\"))))\n                        $command = $subCommand\n                        break\n                    }\n                    \"update\" {\n                        $completer.AddOpts(([Option]::new((\"--pip-args\", \"--head\", \"--pre\"))))\n                        $command = $subCommand\n                        break\n                    }\n                    Default {\n                        $completer.AddParams(@(\"add\", \"remove\", \"list\", \"update\"), $false)\n                    }\n                }\n                break\n            }\n            \"publish\" {\n                $completer.AddOpts(\n                    @(\n                        [Option]::new(@(\n                            \"-r\", \"--repository\", \"-u\", \"--username\", \"-P\", \"--password\", \"-S\", \"--sign\", \"-i\", \"--identity\", \"-c\", \"--comment\",\n                            \"--no-build\", \"--ca-certs\", \"--no-verify-ssl\", \"--skip-existing\", \"-d\", \"--dest\"\n                        )),\n                        $skipOption,\n                        $projectOption\n                    ))\n                break\n            }\n            \"py\" {}\n            \"python\" {\n                $subCommand = $commands[1]\n                switch ($subCommand) {\n                    \"list\" {\n                        $command = $subCommand\n                        break\n                    }\n                    \"remove\" {\n                        $command = $subCommand\n                        break\n                    }\n                    \"install\" {\n                        $completer.AddOpts(([Option]::new((\"--list\", \"--min\"))))\n                        $command = $subCommand\n                        break\n                    }\n                    \"find\" {\n                        $completer.AddOpts(([Option]::new((\"--managed\"))))\n                        $command = $subCommand\n                        break\n                    }\n                    Default {\n                        break\n                    }\n                }\n                break\n            }\n            \"remove\" {\n                $completer.AddOpts(\n                    @(\n                        [Option]::new(@(\n                            \"--global\", \"-g\", \"--dev\", \"-d\", \"--dry-run\", \"--no-sync\", \"--no-editable\", \"--no-self\", \"--override\",\n                            \"-C\", \"--config-setting\", \"--no-isolation\", \"-L\", \"--lockfile\", \"--fail-fast\", \"-x\", \"--frozen-lockfile\"\n                        )),\n                        $projectOption,\n                        $skipOption,\n                        $venvOption,\n                        $sectionOption\n                    ))\n                $completer.AddParams(@(getPdmPackages), $true)\n                break\n            }\n            \"run\" {\n                $completer.AddOpts(\n                    @(\n                        [Option]::new(@(\"--global\", \"-g\", \"-l\", \"--list\", \"-s\", \"--site-packages\", \"--json\", \"--recreate\")),\n                        $skipOption,\n                        $venvOption,\n                        $projectOption\n                    ))\n                $completer.AddParams(@(getScripts), $false)\n                break\n            }\n            \"search\" { break }\n            \"show\" {\n                $completer.AddOpts(\n                    @(\n                        [Option]::new(@(\"--global\", \"-g\", \"--name\", \"--version\", \"--summary\", \"--license\", \"--platform\", \"--keywords\")),\n                        $venvOption,\n                        $projectOption\n                    ))\n                break\n            }\n            \"sync\" {\n                $completer.AddOpts(@(\n                        [Option]::new((\n                            \"-d\", \"--dev\", \"-g\", \"--global\", \"--no-default\", \"--clean\", \"--clean-unselected\", \"--only-keep\", \"--dry-run\",\n                            \"-r\", \"--reinstall\", \"--prod\", \"--production\", \"--no-editable\", \"--no-self\", \"--no-isolation\",\n                            \"-C\", \"--config-setting\", \"-L\", \"--lockfile\", \"--fail-fast\", \"-x\"\n                        )),\n                        $sectionOption,\n                        $venvOption,\n                        $skipOption,\n                        $projectOption\n                    ))\n                break\n            }\n            \"update\" {\n                $completer.AddOpts(@(\n                        [Option]::new((\n                            \"-d\", \"--dev\", \"--save-compatible\", \"--prod\", \"--production\", \"--save-wildcard\", \"--save-exact\",\n                            \"--save-minimum\", \"--update-eager\", \"--update-reuse\", \"--update-all\", \"-g\", \"--global\", \"--dry-run\",\n                            \"--outdated\", \"--top\", \"-u\", \"--unconstrained\", \"--no-editable\", \"--no-self\", \"--no-isolation\",\n                            \"--no-sync\", \"--pre\", \"--prerelease\", \"-L\", \"--lockfile\", \"--fail-fast\", \"-x\", \"--frozen-lockfile\",\n                            \"-C\", \"--config-setting\", \"--update-reuse-installed\", \"--stable\", \"--override\", \"--save-safe-compatible\"\n                        )),\n                        $sectionOption,\n                        $skipOption,\n                        $venvOption,\n                        $projectOption\n                    ))\n                $completer.AddParams(@(getPdmPackages), $true)\n                break\n            }\n            \"use\" {\n                $completer.AddOpts(\n                    @(\n                        [Option]::new(@(\"--global\", \"-g\", \"-f\", \"--first\", \"-i\", \"--ignore-remembered\", \"--skip\", \"--auto-install-min\", \"--auto-install-max\", \"--no-version-file\")),\n                        $venvOption,\n                        $projectOption\n                    ))\n                break\n            }\n            \"venv\" {\n                $subCommand = $commands[1]\n                switch ($subCommand) {\n                    \"create\" {\n                        $command = $subCommand\n                        $completer.AddOpts((\n                            [Option]::new((\"--with\", \"-w\")).WithValues(@(\"venv\", \"virtualenv\", \"conda\")),\n                            [Option]::new((\"--name\", \"-n\")).WithValues(@()),\n                            [Option]::new((\"--with-pip\", \"-f\", \"--force\"))\n                        ))\n                    }\n                    \"list\" {$command = $subCommand}\n                    \"remove\" {\n                        $command = $subCommand\n                        $completer.AddOpts(([Option]::new((\"-y\", \"--yes\"))))\n                    }\n                    \"activate\" {$command = $subCommand}\n                    \"purge\" {\n                        $command = $subCommand\n                        $completer.AddOpts(([Option]::new((\"-i\", \"--interactive\", \"--force\", \"-f\"))))\n                    }\n                    Default {\n                        $completer.AddOpts(([Option]::new((\"--python\", \"--path\"))))\n                        $completer.AddParams(@(\"create\", \"list\", \"remove\", \"activate\", \"purge\"), $false)\n                        break\n                    }\n                }\n                break\n            }\n\n            default {\n                # No command\n                $command = $null\n                $completer.AddOpts(([Option]::new((\"--pep582\", \"-I\", \"--ignore-python\", \"-c\", \"--config\", \"--no-cache\", \"-n\", \"--non-interactive\"))))\n                $completer.AddParams($AllCommands, $false)\n            }\n        }\n        $start = [array]::IndexOf($words, $command) + 1\n        $completer.Complete($words[$start..$words.Length])\n    }\n    elseif (Test-Path Function:\\_pdm_completeBackup) {\n        # Fall back on existing tab expansion\n        _pdm_completeBackup $line $lastWord\n    }\n}\n"
  },
  {
    "path": "src/pdm/cli/completions/pdm.zsh",
    "content": "#compdef pdm\n\nPDM_PYTHON=\"%{python_executable}\"\n\n_pdm() {\n  emulate -L zsh -o extended_glob\n\n  typeset -A opt_args\n  local context state state_descr line\n\n  local curcontext=$curcontext ret=1\n  local -a arguments=(\n    {-h,--help}'[Show help message and exit]'\n    {-v,--verbose}'[Use `-v` for detailed output and `-vv` for more detailed]'\n    {-q,--quiet}'[Suppress output]'\n  )\n  local sub_commands=(\n    'add:Add package(s) to pyproject.toml and install them'\n    'build:Build artifacts for distribution'\n    'cache:Control the caches of PDM'\n    'completion:Generate completion scripts for the given shell'\n    'config:Display the current configuration'\n    'export:Export the locked packages set to other formats'\n    'fix:Fix the project problems according to the latest version of PDM'\n    'import:Import project metadata from other formats'\n    'info:Show the project information'\n    'init:Initialize a pyproject.toml for PDM'\n    'new:Create a new Python project at project_path'\n    'install:Install dependencies from lock file'\n    'list:List packages installed in the current working set'\n    'lock:Resolve and lock dependencies'\n    'self:Manage the PDM program itself (previously known as plugin)'\n    'outdated:Check for outdated packages and list the latest versions on indexes'\n    'publish:Build and publish the project to PyPI'\n    'python:Manage installed Python interpreters'\n    'py:Manage installed Python interpreters'\n    'remove:Remove packages from pyproject.toml'\n    'run:Run commands or scripts with local packages loaded'\n    'search:Search for PyPI packages'\n    'show:Show the package information'\n    'sync:Synchronize the current working set with lock file'\n    'update:Update package(s) in pyproject.toml'\n    'use:Use the given python version or path as base interpreter'\n    'venv:Virtualenv management'\n  )\n\n  _arguments -s -C -A '-*' \\\n    $arguments \\\n    {-c,--config}'[Specify another config file path\\[env var: PDM_CONFIG_FILE\\]]' \\\n    {-V,--version}'[Show the version and exit]' \\\n    {-I,--ignore-python}'[Ignore the Python path saved in .pdm-python]' \\\n    '--no-cache:Disable the cache for the current command. [env var: PDM_NO_CACHE]' \\\n    '--pep582:Print the command line to be eval by the shell for PEP 582:shell:(zsh bash fish tcsh csh)' \\\n    {-n,--non-interactive}\"[Don't show interactive prompts but use defaults. \\[env var: PDM_NON_INTERACTIVE\\]]\" \\\n    '*:: :->_subcmds' \\\n    && return 0\n\n  if (( CURRENT == 1 )); then\n    _describe -t commands 'pdm subcommand' sub_commands\n    return\n  fi\n\n  curcontext=${curcontext%:*}:$words[1]\n\n  case $words[1] in\n    add)\n      arguments+=(\n        {-g,--global}'[Use the global project, supply the project root with `-p` option]'\n        {-d,--dev}'[Add packages into dev dependencies]'\n        {-G,--group}'[Specify the target dependency group to add into]:group:_pdm_groups'\n        {-L,--lockfile}'[Specify another lockfile path, or use `PDM_LOCKFILE` env variable. Default: pdm.lock]:lockfile:_files'\n        \"--override+[Use the constraint file in pip-requirements format for overriding. \\[env var: PDM_CONSTRAINT\\] This option can be used multiple times. See https://pip.pypa.io/en/stable/user_guide/#constraints-files]:override:_files\"\n        '--no-sync[Only write pyproject.toml and do not sync the working set]'\n        '--save-compatible[Save compatible version specifiers]'\n        '--save-wildcard[Save wildcard version specifiers]'\n        '--save-exact[Save exact version specifiers]'\n        '--save-safe-compatible[Save safe compatible version specifiers]'\n        '--save-minimum[Save minimum version specifiers]'\n        '--update-reuse[Reuse pinned versions already present in lock file if possible]'\n        '--update-reuse-installed[Reuse installed packages if possible]'\n        '--update-eager[Try to update the packages and their dependencies recursively]'\n        '--update-all[Update all dependencies and sub-dependencies]'\n        '--no-editable[Install non-editable versions for all packages]'\n        \"--no-self[Don't install the project itself]\"\n        \"--frozen-lockfile[Don't try to create or update the lockfile. \\[env var: PDM_FROZEN_LOCKFILE\\]]\"\n        '--venv[Run the command in the virtual environment with the given key. \\[env var: PDM_IN_VENV\\]]:venv:'\n        {-k,--skip}'[Skip some tasks and/or hooks by their comma-separated names]'\n        {-u,--unconstrained}'[Ignore the version constraints in pyproject.toml and overwrite with new ones from the resolution result]'\n        {--pre,--prerelease}'[Allow prereleases to be pinned]'\n        \"--stable[Only allow stable versions to be pinned]\"\n        {-e+,--editable+}'[Specify editable packages]:packages'\n        {-x,--fail-fast}'[Abort on first installation error]'\n        {-C,--config-setting}'[Pass options to the backend. options with a value must be specified after \"=\": \"--config-setting=key(=value)\" or \"-Ckey(=value)\"]:cs:'\n        \"--no-isolation[do not isolate the build in a clean environment]\"\n        \"--dry-run[Show the difference only without modifying the lockfile content]\"\n      )\n      ;;\n    build)\n      arguments+=(\n        \"--no-sdist[Don't build source tarballs]\"\n        \"--no-wheel[Don't build wheels]\"\n        {-d+,--dest+}'[Target directory to put artifacts]:directory:_files -/'\n        {-k,--skip}'[Skip some tasks and/or hooks by their comma-separated names]'\n        '--no-clean[Do not clean the target directory]'\n        {-C,--config-setting}'[Pass options to the backend. options with a value must be specified after \"=\": \"--config-setting=key(=value)\" or \"-Ckey(=value)\"]:cs:'\n        \"--no-isolation[do not isolate the build in a clean environment]\"\n      )\n      ;;\n    cache)\n      _arguments -C \\\n        $arguments \\\n        ': :->command' \\\n        '*:: :->args' && ret=0\n      case $state in\n        command)\n          local -a actions=(\n            \"clear:Clean all the files under cache directory\"\n            \"remove:Remove files matching the given pattern\"\n            \"list:List the built wheels stored in the cache\"\n            \"info:Show the info and current size of caches\"\n          )\n          _describe -t command 'pdm cache actions' actions && ret=0\n          ;;\n        args)\n          case $words[1] in\n            clear)\n              compadd -X type 'hashes' 'http' 'wheels' 'metadata' 'packages' && ret=0\n              ;;\n            *)\n              _message \"pattern\" && ret=0\n              ;;\n          esac\n          ;;\n      esac\n      return $ret\n      ;;\n    config)\n      _arguments -s  \\\n         {-g,--global}'[Use the global project, supply the project root with `-p` option]' \\\n         {-l,--local}\"[Set config in the project's local configuration file]\" \\\n         {-d,--delete}'[Unset a configuration key]' \\\n         {-e,--edit}'[Edit the configuration file in the default editor(defined by EDITOR env var)]' \\\n         '1:key:->keys' \\\n         '2:value:_files' && return 0\n      if [[ $state == keys ]]; then\n        local l mbegin mend match keys=()\n        for l in ${(f)\"$(PDM_CHECK_UPDATE=0 command ${PDM_PYTHON} -m pdm config)\"}; do\n          if [[ $l == (#b)\" \"#(*)\" = \"(*) ]]; then\n            keys+=(\"$match[1]:$match[2]\")\n          fi\n        done\n        _describe -t key \"key\" keys && return 0\n      fi\n      ;;\n    export)\n      arguments+=(\n        {-g,--global}'[Use the global project, supply the project root with `-p` option]'\n        {-f+,--format+}\"[Only requirements.txt is supported for now.]:format:(requirements)\"\n        \"--no-hashes[Don't include artifact hashes]\"\n        \"--no-markers[Don't include platform markers]\"\n        \"--no-extras[Strip extras from the requirements]\"\n        \"--expandvars[Expand environment variables in requirements]\"\n        \"--self[Include the project itself]\"\n        \"--editable-self[Include the project itself as an editable dependency]\"\n        {-L,--lockfile}'[Specify another lockfile path, or use `PDM_LOCKFILE` env variable. Default: pdm.lock]:lockfile:_files'\n        {-o+,--output+}\"[Write output to the given file, or print to stdout if not given]:output file:_files\"\n        {-G+,--group+,--with+}'[Select group of optional-dependencies or dev-dependencies(with -d). Can be supplied multiple times, use \":all\" to include all groups under the same species]:group:_pdm_groups'\n        \"--without+[Exclude groups of optional-dependencies or dev-dependencies]:group:_pdm_groups\"\n        {-d,--dev}\"[Select dev dependencies]\"\n        {--prod,--production}\"[Unselect dev dependencies]\"\n        \"--no-default[Don't include dependencies from the default group]\"\n      )\n      ;;\n    fix)\n      arguments+=(\n        {-g,--global}'[Use the global project, supply the project root with `-p` option]'\n        '--dry-run[Only show the problems]'\n        '1:problem:'\n      )\n      ;;\n    import)\n      arguments+=(\n        {-g,--global}'[Use the global project, supply the project root with `-p` option]'\n        {-f+,--format+}\"[Specify the file format explicitly]:format:(pipfile poetry flit requirements)\"\n        '1:filename:_files'\n      )\n      ;;\n    info)\n      arguments+=(\n        {-g,--global}'[Use the global project, supply the project root with `-p` option]'\n        '--python[Show the interpreter path]'\n        '--where[Show the project root path]'\n        '--env[Show PEP 508 environment markers]'\n        '--packages[Show the packages root]'\n        '--json[Dump the information in JSON]'\n        '--venv[Run the command in the virtual environment with the given key. \\[env var: PDM_IN_VENV\\]]:venv:'\n      )\n      ;;\n    new|init)\n      arguments+=(\n        {-g,--global}'[Use the global project, supply the project root with `-p` option]'\n        {-n,--non-interactive}\"[Don't ask questions but use default values]\"\n        {-k,--skip}'[Skip some tasks and/or hooks by their comma-separated names]'\n        {-r,--overwrite}'[Overwrite existing files]'\n        '--backend[Specify the build backend, which implies --dist]:backend:(pdm-backend setuptools hatchling flit)'\n        {--dist,--lib}'[Create a package for distribution]'\n        '--python[Specify the Python version/path to use]:python:'\n        '--copier[Use Copier to generate project]'\n        '--cookiecutter[Use Cookiecutter to generate project]'\n        '--license[Specify the license (SPDX name)]:license:'\n        '--name[Specify the project name]:name:'\n        '--no-git[Do not initialize a git repository]'\n        \"--project-version[Specify the project's version]:project_version:\"\n        '1:template:'\n      )\n      ;;\n    install)\n      arguments+=(\n        {-g,--global}'[Use the global project, supply the project root with `-p` option]'\n        {-G+,--group+,--with+}'[Select group of optional-dependencies or dev-dependencies(with -d). Can be supplied multiple times, use \":all\" to include all groups under the same species]:group:_pdm_groups'\n        \"--without+[Exclude groups of optional-dependencies or dev-dependencies]:group:_pdm_groups\"\n        {-d,--dev}\"[Select dev dependencies]\"\n        {-L,--lockfile}'[Specify another lockfile path, or use `PDM_LOCKFILE` env variable. Default: pdm.lock]:lockfile:_files'\n        \"--override+[Use the constraint file in pip-requirements format for overriding. \\[env var: PDM_CONSTRAINT\\] This option can be used multiple times. See https://pip.pypa.io/en/stable/user_guide/#constraints-files]:override:_files\"\n        {--prod,--production}\"[Unselect dev dependencies]\"\n        {-k,--skip}'[Skip some tasks and/or hooks by their comma-separated names]'\n        \"--frozen-lockfile[Don't try to create or update the lockfile. \\[env var: PDM_FROZEN_LOCKFILE\\]]\"\n        \"--no-default[Don\\'t include dependencies from the default group]\"\n        '--no-editable[Install non-editable versions for all packages]'\n        \"--no-self[Don't install the project itself]\"\n        {-x,--fail-fast}'[Abort on first installation error]'\n        {-C,--config-setting}'[Pass options to the backend. options with a value must be specified after \"=\": \"--config-setting=key(=value)\" or \"-Ckey(=value)\"]:cs:'\n        \"--no-isolation[do not isolate the build in a clean environment]\"\n        \"--dry-run[Show the difference only without modifying the lock file content]\"\n        \"--check[Check if the lock file is up to date and fail otherwise]\"\n        \"--plugins[Install the plugins specified in pyproject.toml]\"\n        '--venv[Run the command in the virtual environment with the given key. \\[env var: PDM_IN_VENV\\]]:venv:'\n      )\n      ;;\n    list)\n      arguments+=(\n        {-g,--global}'[Use the global project, supply the project root with `-p` option]'\n        {-r,--reverse}'[Reverse the dependency tree]'\n        '--fields[Select information to output as a comma separated string.]:fields:'\n        \"--sort[Sort the output using a given field name. If nothing is set, no sort is applied. Multiple fields can be combined with ',']:sort:\"\n        '--json[Output dependencies in JSON document format]'\n        '--csv[Output dependencies in CSV document format]'\n        '--markdown[Output dependencies and legal notices in markdown document format - best effort basis]'\n        {--tree,--graph}'[Display a tree of dependencies]'\n        \"--freeze[Show the installed dependencies as pip's requirements.txt format]\"\n        \"--include[Dependency groups to include in the output. By default all are included]:include:\"\n        \"--exclude[Dependency groups to exclude from the output]:exclude:\"\n        \"--resolve[Resolve all requirements to output licenses (instead of just showing those currently installed)]\"\n        '--venv[Run the command in the virtual environment with the given key. \\[env var: PDM_IN_VENV\\]]:venv:'\n        '*:patterns:'\n      )\n      ;;\n    lock)\n      arguments+=(\n        {-g,--global}'[Use the global project, supply the project root with `-p` option]'\n        {-L,--lockfile}'[Specify another lockfile path, or use `PDM_LOCKFILE` env variable. Default: pdm.lock]:lockfile:_files'\n        {-C,--config-setting}'[Pass options to the backend. options with a value must be specified after \"=\": \"--config-setting=key(=value)\" or \"-Ckey(=value)\"]:cs:'\n        \"--no-isolation[Do not isolate the build in a clean environment]\"\n        {-k,--skip}'[Skip some tasks and/or hooks by their comma-separated names]'\n        \"--refresh[Refresh the content hash and file hashes in the lock file]\"\n        \"--check[Check if the lock file is up to date and quit]\"\n        {-G+,--group+,--with+}'[Select group of optional-dependencies or dev-dependencies(with -d). Can be supplied multiple times, use \":all\" to include all groups under the same species]:group:_pdm_groups'\n        \"--without+[Exclude groups of optional-dependencies or dev-dependencies]:group:_pdm_groups\"\n        \"--override+[Use the constraint file in pip-requirements format for overriding. \\[env var: PDM_CONSTRAINT\\] This option can be used multiple times. See https://pip.pypa.io/en/stable/user_guide/#constraints-files]:override:_files\"\n        {-d,--dev}\"[Select dev dependencies]\"\n        {--prod,--production}\"[Unselect dev dependencies]\"\n        '--update-reuse[Reuse pinned versions already present in lock file if possible]'\n        '--update-reuse-installed[Reuse installed packages if possible]'\n        \"--static-urls[(DEPRECATED) Store static file URLs in the lockfile]\"\n        \"--no-static-urls[(DEPRECATED) Do not store static file URLs in the lockfile]\"\n        \"--no-default[Don\\'t include dependencies from the default group]\"\n        \"--no-cross-platform[(DEPRECATED) Only lock packages for the current platform]\"\n        \"--exclude-newer[Exclude packages newer than the given UTC date in format YYYY-MM-DD\\[THH:MM:SSZ\\]]:exclude-newer:\"\n        {-S,--strategy}'[Specify lock strategy(cross_platform,static_urls,direct_minimal_versions). Add no_ prefix to disable. Support given multiple times or split by comma.]:strategy:_pdm_lock_strategy'\n        \"--append[Append the result to the current lock file]\"\n        \"--python[The Python range to lock for. E.g. >=3.9, ==3.12.*]:python:\"\n        \"--implementation[The Python implementation to lock for. E.g. cpython, pypy]:implementation:\"\n        \"--platform[The platform to lock for. E.g. linux, windows, macos, alpine, windows_amd64]:platform:_pdm_lock_platform\"\n      )\n      ;;\n    outdated)\n      arguments+=(\n        '--json[Output in JSON format]'\n        '*:patterns:'\n      )\n      ;;\n    self)\n      _arguments -C \\\n        $arguments \\\n        ': :->command' \\\n        '*:: :->args' && ret=0\n      case $state in\n        command)\n          local -a actions=(\n            \"add:Install packages to the PDM's environment\"\n            \"remove:Remove packages from PDM's environment\"\n            \"list:List all packages installed with PDM\"\n            \"update:Update PDM itself\"\n          )\n          _describe -t command 'pdm self actions' actions && ret=0\n          ;;\n        args)\n          case $words[1] in\n            add)\n              arguments+=(\n                '--pip-args[Arguments that will be passed to pip install]:pip args:'\n              )\n              ;;\n            remove)\n              arguments+=(\n                '--pip-args[Arguments that will be passed to pip uninstall]:pip args:'\n                {-y,--yes}'[Answer yes on the question]'\n              )\n              ;;\n            list)\n              arguments+=(\n                '--plugins[List plugins only]'\n                '*:patterns:'\n              )\n              ;;\n            update)\n              arguments+=(\n                '--head[Update to the latest commit on the main branch]'\n                '--pre[Update to the latest prerelease version]'\n                '--pip-args[Arguments that will be passed to pip uninstall]:pip args:'\n              )\n              ;;\n            *)\n              ;;\n          esac\n          ;;\n      esac\n      return $ret\n      ;;\n    python|py)\n      _arguments -C \\\n        $arguments \\\n        ': :->command' \\\n        '*:: :->args' && ret=0\n      case $state in\n        command)\n          local -a actions=(\n            \"remove:Remove a Python interpreter installed with PDM\"\n            \"list:List all Python interpreters installed with PDM\"\n            \"install:Install a Python interpreter with PDM\"\n            \"find:Search for a Python interpreter\"\n          )\n          _describe -t command 'pdm python actions' actions && ret=0\n          ;;\n        args)\n          case $words[1] in\n            remove)\n              arguments+=(\n                ':python:'\n              )\n              ;;\n            install)\n              arguments+=(\n                '--list[List all available Python versions]'\n                '--min[Use minimum instead of highest version for installation if `version` is left empty]'\n                ':python:_files'\n              )\n              ;;\n            find)\n              arguments+=(\n                '--managed[Only find interpreters managed by PDM]'\n                ':request:'\n              )\n              ;;\n            *)\n              ;;\n          esac\n          ;;\n        esac\n      return $ret\n      ;;\n    publish)\n      arguments+=(\n        {-r,--repository}'[The repository name or url to publish the package to }\\[env var: PDM_PUBLISH_REPO\\]]:repository:'\n        {-u,--username}'[The username to access the repository \\[env var: PDM_PUBLISH_USERNAME\\]]:username:'\n        {-P,--password}'[The password to access the repository \\[env var: PDM_PUBLISH_PASSWORD\\]]:password:'\n        {-S,--sign}'[Upload the package with PGP signature]'\n        {-i,--identity}'[GPG identity used to sign files.]:gpg identity:'\n        {-k,--skip}'[Skip some tasks and/or hooks by their comma-separated names]'\n        {-c,--comment}'[The comment to include with the distribution file.]:comment:'\n        {-d,--dest}'[The directory to upload the package from]:dest:_files'\n        \"--no-verify-ssl[Disable SSL verification]\"\n        \"--ca-certs[The path to a PEM-encoded Certificate Authority bundle to use for publish server validation]:cacerts:_files\"\n        \"--no-build[Don't build the package before publishing]\"\n        \"--skip-existing[Skip uploading files that already exist. This may not work with some repository implementations.]\"\n      )\n      ;;\n    remove)\n      arguments+=(\n        {-g,--global}'[Use the global project, supply the project root with `-p` option]'\n        {-G,--group}'[Specify the target dependency group to remove from]:group:_pdm_groups'\n        {-d,--dev}\"[Remove packages from dev dependencies]\"\n        {-L,--lockfile}'[Specify another lockfile path, or use `PDM_LOCKFILE` env variable. Default: pdm.lock]:lockfile:_files'\n        \"--override+[Use the constraint file in pip-requirements format for overriding. \\[env var: PDM_CONSTRAINT\\] This option can be used multiple times. See https://pip.pypa.io/en/stable/user_guide/#constraints-files]:override:_files\"\n        {-k,--skip}'[Skip some tasks and/or hooks by their comma-separated names]'\n        \"--no-sync[Only write pyproject.toml and do not uninstall packages]\"\n        '--no-editable[Install non-editable versions for all packages]'\n        \"--no-self[Don't install the project itself]\"\n        \"--frozen-lockfile[Don't try to create or update the lockfile. \\[env var: PDM_FROZEN_LOCKFILE\\]]\"\n        {-x,--fail-fast}'[Abort on first installation error]'\n        {-C,--config-setting}'[Pass options to the backend. options with a value must be specified after \"=\": \"--config-setting=key(=value)\" or \"-Ckey(=value)\"]:cs:'\n        \"--no-isolation[do not isolate the build in a clean environment]\"\n        \"--dry-run[Show the difference only without modifying the lockfile content]\"\n        '--venv[Run the command in the virtual environment with the given key. \\[env var: PDM_IN_VENV\\]]:venv:'\n        \"*:packages:_pdm_packages\"\n      )\n      ;;\n    run)\n      _arguments -s \\\n        {-g,--global}'[Use the global project, supply the project root with `-p` option]' \\\n        {-l,--list}'[Show all available scripts defined in pyproject.toml]' \\\n        '--json[Output all scripts infos in JSON]' \\\n        '--recreate[Recreate the script environment for self-contained scripts]' \\\n        {-k,--skip}'[Skip some tasks and/or hooks by their comma-separated names]' \\\n        {-s,--site-packages}'[Load site-packages from the selected interpreter]' \\\n        '--venv[Run the command in the virtual environment with the given key. \\[env var: PDM_IN_VENV\\]]:venv:' \\\n        '(-)1:command:->command' \\\n        '*:arguments: _normal ' && return 0\n      if [[ $state == command ]]; then\n        _command_names -e\n        local local_commands=($(_pdm_scripts))\n        _describe \"local command\" local_commands\n        return 0\n      fi\n      ;;\n    search)\n      arguments+=(\n        '1:query string:'\n      )\n      ;;\n    show)\n      arguments+=(\n        {-g,--global}'[Use the global project, supply the project root with `-p` option]'\n        '--name[Show name]'\n        '--version[Show version]'\n        '--summary[Show summary]'\n        '--license[Show license]'\n        '--platform[Show platform]'\n        '--keywords[Show keywords]'\n        '--venv[Run the command in the virtual environment with the given key. \\[env var: PDM_IN_VENV\\]]:venv:'\n        '1:package:'\n      )\n      ;;\n    sync)\n      arguments+=(\n        {-g,--global}'[Use the global project, supply the project root with `-p` option]'\n        {-G+,--group+,--with+}'[Select group of optional-dependencies or dev-dependencies(with -d). Can be supplied multiple times, use \":all\" to include all groups under the same species]:group:_pdm_groups'\n        \"--without+[Exclude groups of optional-dependencies or dev-dependencies]:group:_pdm_groups\"\n        {-d,--dev}\"[Select dev dependencies]\"\n        {-L,--lockfile}'[Specify another lockfile path, or use `PDM_LOCKFILE` env variable. Default: pdm.lock]:lockfile:_files'\n        {--prod,--production}\"[Unselect dev dependencies]\"\n        {-k,--skip}'[Skip some tasks and/or hooks by their comma-separated names]'\n        '--dry-run[Only prints actions without actually running them]'\n        {-r,--reinstall}\"[Force reinstall existing dependencies]\"\n        '--clean[Clean unused packages]'\n        \"--clean-unselected[Remove all but the selected packages]\"\n        \"--only-keep[Remove all but the selected packages]\"\n        \"--no-default[Don\\'t include dependencies from the default group]\"\n        {-x,--fail-fast}'[Abort on first installation error]'\n        '--no-editable[Install non-editable versions for all packages]'\n        \"--no-self[Don't install the project itself]\"\n        {-C,--config-setting}'[Pass options to the backend. options with a value must be specified after \"=\": \"--config-setting=key(=value)\" or \"-Ckey(=value)\"]:cs:'\n        \"--no-isolation[do not isolate the build in a clean environment]\"\n        '--venv[Run the command in the virtual environment with the given key. \\[env var: PDM_IN_VENV\\]]:venv:'\n      )\n      ;;\n    update)\n      arguments+=(\n        {-g,--global}'[Use the global project, supply the project root with `-p` option]'\n        {-G+,--group+,--with+}'[Select group of optional-dependencies or dev-dependencies(with -d). Can be supplied multiple times, use \":all\" to include all groups under the same species]:group:_pdm_groups'\n        \"--without+[Exclude groups of optional-dependencies or dev-dependencies]:group:_pdm_groups\"\n        {-L,--lockfile}'[Specify another lockfile path, or use `PDM_LOCKFILE` env variable. Default: pdm.lock]:lockfile:_files'\n        \"--override+[Use the constraint file in pip-requirements format for overriding. \\[env var: PDM_CONSTRAINT\\] This option can be used multiple times. See https://pip.pypa.io/en/stable/user_guide/#constraints-files]:override:_files\"\n        '--save-compatible[Save compatible version specifiers]'\n        '--save-wildcard[Save wildcard version specifiers]'\n        '--save-exact[Save exact version specifiers]'\n        '--save-minimum[Save minimum version specifiers]'\n        '--save-safe-compatible[Save safe compatible version specifiers]'\n        '--update-reuse[Reuse pinned versions already present in lock file if possible]'\n        '--update-eager[Try to update the packages and their dependencies recursively]'\n        '--update-all[Update all dependencies and sub-dependencies]'\n        '--update-reuse-installed[Reuse installed packages if possible]'\n        '--no-editable[Install non-editable versions for all packages]'\n        \"--no-self[Don't install the project itself]\"\n        \"--no-sync[Only update lock file but do not sync packages]\"\n        \"--frozen-lockfile[Don't try to create or update the lockfile. \\[env var: PDM_FROZEN_LOCKFILE\\]]\"\n        {-k,--skip}'[Skip some tasks and/or hooks by their comma-separated names]'\n        {-u,--unconstrained}'[Ignore the version constraints in pyproject.toml and overwrite with new ones from the resolution result]'\n        {--pre,--prerelease}'[Allow prereleases to be pinned]'\n        \"--stable[Only allow stable versions to be pinned]\"\n        {-d,--dev}'[Select dev dependencies]'\n        {--prod,--production}\"[Unselect dev dependencies]\"\n        \"--no-default[Don\\'t include dependencies from the default group]\"\n        {-t,--top}'[Only update those list in pyproject.toml]'\n        \"--dry-run[Show the difference only without modifying the lockfile content]\"\n        \"--outdated[Show the difference only without modifying the lockfile content]\"\n        {-x,--fail-fast}'[Abort on first installation error]'\n        {-C,--config-setting}'[Pass options to the backend. options with a value must be specified after \"=\": \"--config-setting=key(=value)\" or \"-Ckey(=value)\"]:cs:'\n        \"--no-isolation[do not isolate the build in a clean environment]\"\n        '--venv[Run the command in the virtual environment with the given key. \\[env var: PDM_IN_VENV\\]]:venv:'\n        \"*:packages:_pdm_packages\"\n      )\n      ;;\n    use)\n      arguments+=(\n        {-f,--first}'[Select the first matched interpreter -- no auto install]'\n        '--auto-install-min[If `python` argument not given, auto install minimum best match - otherwise has no effect]'\n        '--auto-install-max[If `python` argument not given, auto install maximum best match - otherwise has no effect]'\n        {-i,--ignore-remembered}'[Ignore the remembered selection]'\n        '--venv[Use the interpreter in the virtual environment with the given name]:venv:'\n        '--no-version-file[Do not write the version file]'\n        '*:python:_files'\n      )\n      ;;\n    venv)\n      _arguments -C \\\n        $arguments \\\n        ': :->command' \\\n        '*:: :->args' && ret=0\n      case $state in\n        command)\n          local -a actions=(\n            \"create:Create a virtualenv\"\n            \"list:List all virtualenvs associated with this project\"\n            \"remove:Remove the virtualenv with the given name\"\n            \"activate:Activate the virtualenv with the given name\"\n            \"purge:Purge selected/all created Virtualenvs\"\n          )\n          arguments+=(\n            '--path[Show the path to the given virtualenv]'\n            '--python[Show the Python interpreter path of the given virtualenv]'\n          )\n          _describe -t command 'pdm venv actions' actions && ret=0\n          ;;\n        args)\n          case $words[1] in\n            create)\n              arguments+=(\n                {-w,--with}'[Specify the backend to create the virtualenv]:backend:(virtualenv venv conda)'\n                '--with-pip[Install pip with the virtualenv]'\n                {-n,--name}'[Specify the name of the virtualenv]:name:'\n                {-f,--force}'[Recreate if the virtualenv already exists]'\n              )\n              ;;\n            remove)\n              arguments+=(\n                {-y,--yes}'[Answer yes on the following question]'\n              )\n              ;;\n            purge)\n              arguments+=(\n                {-f,--force}'[Force purging without prompting for confirmation]'\n                {-i,--interactive}'[Interactively purge selected Virtualenvs]'\n              )\n              ;;\n            *)\n              ;;\n          esac\n          ;;\n      esac\n      return $ret\n      ;;\n  esac\n\n  _arguments -s $arguments && ret=0\n\n  return ret\n}\n\n_pdm_groups() {\n  if [[ ! -f pyproject.toml ]]; then\n    _message \"not a pdm project\"\n    return 1\n  fi\n  local l groups=() in_groups=0\n  while IFS= read -r l; do\n    case $l in\n      \"[\"project.optional-dependencies\"]\") in_groups=1 ;;\n      \"[\"tool.pdm.dev-dependencies\"]\") in_groups=1 ;;\n      \"[\"*\"]\") in_groups=0 ;;\n      *\"= [\")\n        if (( in_groups )); then\n          groups+=$l[(w)1]\n        fi\n        ;;\n    esac\n  done <pyproject.toml\n  compadd -X groups -a groups\n}\n\n_get_packages_with_python() {\n  command ${PDM_PYTHON} - << EOF\nimport sys\nif sys.version_info >= (3, 11):\n  import tomllib\nelse:\n  import tomli as tomllib\nimport os, re\nPACKAGE_REGEX = re.compile(r'^[A-Za-z][A-Za-z0-9._-]*')\ndef get_packages(lines):\n    return [PACKAGE_REGEX.match(line).group() for line in lines]\n\nwith open('pyproject.toml', 'rb') as f:\n    data = tomllib.load(f)\npackages = get_packages(data.get('project', {}).get('dependencies', []))\nfor reqs in data.get('project', {}).get('optional-dependencies', {}).values():\n    packages.extend(get_packages(reqs))\nfor reqs in data.get('tool', {}).get('pdm', {}).get('dev-dependencies', {}).values():\n    packages.extend(get_packages(reqs))\nprint(*set(packages))\nEOF\n}\n\n_pdm_scripts() {\n  local scripts=() package_dir=$(PDM_CHECK_UPDATE=0 $PDM_PYTHON -m pdm info --packages)\n  if [[ -f pyproject.toml ]]; then\n    local l in_scripts=0\n    while IFS= read -r l; do\n    case $l in\n      \"[\"tool.pdm.scripts\"]\") in_scripts=1 ;;\n      \"[\"*\"]\") in_scripts=0 ;;\n      *\"= \"*)\n        if (( in_scripts )); then\n            scripts+=$l[(w)1]\n        fi\n      ;;\n    esac\n    done < pyproject.toml\n  fi\n  if [[ $package_dir != \"None\" ]]; then\n    scripts+=($package_dir/bin/*(N:t))\n  fi\n  echo $scripts\n}\n\n_pdm_packages() {\n  if [[ ! -f pyproject.toml ]]; then\n    _message \"not a pdm project\"\n    return 1\n  fi\n  local packages=(${=$(_get_packages_with_python)})\n  compadd -X packages -a packages\n}\n\n_pdm_lock_strategy() {\n  local -a strategy=(\n    'cross_platform:(DEPRECATED)Lock packages for all platforms'\n    'inherit_metadata:Calculate and store the markers for the packages'\n    'static_urls:Store static file URLs in the lockfile'\n    'direct_minimal_versions:Store the minimal versions of the dependencies'\n    'no_cross_platform:Only lock packages for the current platform'\n    'no_static_urls:Do not store static file URLs in the lockfile'\n    'no_inherit_metadata:Do not calculate and store the markers for the packages'\n    'no_direct_minimal_versions:Do not store the minimal versions of the dependencies'\n  )\n  _describe -t strategy \"lock strategy\" strategy\n}\n\n_pdm_lock_platform() {\n  local -a platforms=(\n    \"linux\"\n    \"windows\"\n    \"macos\"\n    \"alpine\"\n    \"windows_amd64\"\n    \"windows_x86\"\n    \"windows_arm64\"\n    \"macos_arm64\"\n    \"macos_x86_64\"\n  )\n  _describe -t platform \"platform\" platforms\n}\n\n_pdm \"$@\"\n"
  },
  {
    "path": "src/pdm/cli/filters.py",
    "content": "from __future__ import annotations\n\nimport argparse\nfrom functools import cached_property\nfrom typing import TYPE_CHECKING\n\nfrom pdm.exceptions import PdmUsageError\nfrom pdm.utils import normalize_name\n\nif TYPE_CHECKING:\n    from typing import Iterator, Sequence\n\n    from pdm.project import Project\n\n\nclass GroupSelection:\n    def __init__(\n        self,\n        project: Project,\n        *,\n        default: bool = True,\n        dev: bool | None = None,\n        groups: Sequence[str] = (),\n        group: str | None = None,\n        excluded_groups: Sequence[str] = (),\n        exclude_non_existing: bool = False,\n    ):\n        self.project = project\n        self.groups = groups\n        self.group = group\n        self.default = default\n        self.dev = dev\n        self.excluded_groups = excluded_groups\n        self.exclude_non_existing = exclude_non_existing\n\n    @classmethod\n    def from_options(cls, project: Project, options: argparse.Namespace) -> GroupSelection:\n        if getattr(options, \"excluded_groups\", None) and not options.groups and options.dev is None:\n            options.groups = [\":all\"]\n        if \"group\" in options:\n            return cls(project, group=options.group, dev=options.dev)\n        return cls(\n            project,\n            default=options.default,\n            dev=options.dev,\n            groups=options.groups,\n            excluded_groups=options.excluded_groups,\n        )\n\n    def one(self) -> str:\n        if self.group:\n            return self.group\n        if len(self.groups) == 1:\n            return self.groups[0]\n        return \"dev\" if self.dev else \"default\"\n\n    @property\n    def is_unset(self) -> bool:\n        return self.default and self.dev is None and not self.groups\n\n    def all(self) -> list[str] | None:\n        project_groups = list(self.project.iter_groups())\n        if self.is_unset:\n            if self.project.lockfile.exists():\n                groups = self.project.lockfile.groups\n                if groups:\n                    groups = [g for g in groups if g in project_groups]\n                return groups\n        return list(self)\n\n    @cached_property\n    def _translated_groups(self) -> list[str]:\n        \"\"\"Translate default, dev and groups containing \":all\" into a list of groups\"\"\"\n        locked_groups = self.project.lockfile.groups\n        if self.is_unset:\n            # Default case, return what is in the lock file\n            project_groups = list(self.project.iter_groups())\n            if locked_groups:\n                return [g for g in locked_groups if g in project_groups]\n        default, dev, groups = self.default, self.dev, self.groups\n        if dev is None:  # --prod is not set, include dev-dependencies\n            dev = True\n        project = self.project\n        optional_groups = {normalize_name(g) for g in project.pyproject.metadata.get(\"optional-dependencies\", {})}\n        dev_groups = set(project.pyproject.dev_dependencies)\n        groups_set = {normalize_name(g) if g != \":all\" else g for g in groups}\n        if groups_set & dev_groups:\n            if not dev:\n                raise PdmUsageError(\"--prod is not allowed with dev groups and should be left\")\n        elif dev:\n            groups_set.update(dev_groups)\n            if self.exclude_non_existing and locked_groups:\n                groups_set.intersection_update(locked_groups)\n        if \":all\" in groups:\n            groups_set.discard(\":all\")\n            groups_set.update(optional_groups)\n        if default:\n            groups_set.add(\"default\")\n        groups_set -= {normalize_name(g) for g in self.excluded_groups}\n\n        invalid_groups = groups_set - {normalize_name(g) for g in project.iter_groups()}\n        if invalid_groups:\n            project.core.ui.echo(\n                f\"[d]Ignoring non-existing groups: [success]{', '.join(invalid_groups)}[/]\",\n                err=True,\n            )\n            groups_set -= invalid_groups\n        # Sorts the result in ascending order instead of in random order\n        # to make this function pure\n        result = sorted(groups_set, key=lambda x: (x != \"default\", x))\n        return result\n\n    def validate(self) -> None:\n        extra_groups = self.project.lockfile.compare_groups(self._translated_groups)\n        if extra_groups:\n            raise PdmUsageError(f\"Requested groups not in lockfile: {','.join(extra_groups)}\")\n\n    def __iter__(self) -> Iterator[str]:\n        return iter(self._translated_groups)\n\n    def __contains__(self, group: str) -> bool:\n        return group in self._translated_groups\n"
  },
  {
    "path": "src/pdm/cli/hooks.py",
    "content": "from __future__ import annotations\n\nimport contextlib\nfrom typing import Any, Generator\n\nfrom pdm.project.core import Project\nfrom pdm.signals import pdm_signals\n\n\nclass HookManager:\n    def __init__(self, project: Project, skip: list[str] | None = None):\n        self.project = project\n        self.skip = skip or []\n\n    @contextlib.contextmanager\n    def skipping(self, *names: str) -> Generator[None]:\n        \"\"\"\n        Temporarily skip some hooks.\n        \"\"\"\n        old_skip = self.skip[:]\n        self.skip.extend(names)\n        yield\n        self.skip = old_skip\n\n    @property\n    def skip_all(self) -> bool:\n        return \":all\" in self.skip\n\n    @property\n    def skip_pre(self) -> bool:\n        return \":pre\" in self.skip\n\n    @property\n    def skip_post(self) -> bool:\n        return \":post\" in self.skip\n\n    def should_run(self, name: str) -> bool:\n        \"\"\"\n        Tells whether a task given its name should run or not\n        according to the current skipping rules.\n        \"\"\"\n        return (\n            not self.skip_all\n            and name not in self.skip\n            and not (self.skip_pre and name.startswith(\"pre_\"))\n            and not (self.skip_post and name.startswith(\"post_\"))\n        )\n\n    def try_emit(self, name: str, **kwargs: Any) -> None:\n        \"\"\"\n        Emit a hook signal if rules allow it.\n        \"\"\"\n        if self.should_run(name):\n            pdm_signals.signal(name).send(self.project, hooks=self, **kwargs)\n"
  },
  {
    "path": "src/pdm/cli/options.py",
    "content": "from __future__ import annotations\n\nimport argparse\nimport os\nimport sys\nfrom functools import partial\nfrom typing import TYPE_CHECKING, cast\n\nif TYPE_CHECKING:\n    from typing import Any, Protocol, Sequence\n\n    from pdm.project import Project\n\n    class ActionCallback(Protocol):\n        def __call__(\n            self,\n            project: Project,\n            namespace: argparse.Namespace,\n            values: Any,\n            option_string: str | None = None,\n        ) -> None: ...\n\n\nclass Option:\n    \"\"\"A reusable option object which delegates all arguments\n    to parser.add_argument().\n    \"\"\"\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        self.args = args\n        self.kwargs = kwargs\n\n    def add_to_parser(self, parser: argparse._ActionsContainer) -> None:\n        parser.add_argument(*self.args, **self.kwargs)\n\n    def add_to_group(self, group: argparse._ArgumentGroup) -> None:\n        group.add_argument(*self.args, **self.kwargs)\n\n    def __call__(self, func: ActionCallback) -> Option:\n        self.kwargs.update(action=CallbackAction, callback=func)\n        return self\n\n\nclass CallbackAction(argparse.Action):\n    def __init__(self, *args: Any, callback: ActionCallback, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.callback = callback\n\n    def __call__(\n        self,\n        parser: argparse.ArgumentParser,\n        namespace: argparse.Namespace,\n        values: Any,\n        option_string: str | None = None,\n    ) -> None:\n        if not hasattr(namespace, \"callbacks\"):\n            namespace.callbacks = []\n        callback = partial(self.callback, values=values, option_string=option_string)\n        namespace.callbacks.append(callback)\n\n\nclass ExtendMapAction(argparse._AppendAction):\n    def __call__(\n        self,\n        parser: argparse.ArgumentParser,\n        namespace: argparse.Namespace,\n        values: str | Sequence[Any] | None,\n        option_string: str | None = None,\n    ) -> None:\n        assert isinstance(values, str)\n        k, _, v = values.partition(\"=\")\n        mapping = getattr(namespace, self.dest, None) or {}\n        if k in mapping:\n            if not isinstance(mapping[k], list):\n                mapping[k] = [mapping[k]]\n            mapping[k].append(v)\n        else:\n            mapping[k] = v\n        setattr(namespace, self.dest, mapping)\n\n\nclass ArgumentGroup(Option):\n    \"\"\"A reusable argument group object which can call `add_argument()`\n    to add more arguments. And itself will be registered to the parser later.\n    \"\"\"\n\n    def __init__(self, name: str, is_mutually_exclusive: bool = False, required: bool = False) -> None:\n        self.name = name\n        self.options: list[Option] = []\n        self.required = required\n        self.is_mutually_exclusive = is_mutually_exclusive\n\n    def add_argument(self, *args: Any, **kwargs: Any) -> None:\n        if args and isinstance(args[0], Option):\n            self.options.append(args[0])\n        else:\n            self.options.append(Option(*args, **kwargs))\n\n    def add_to_parser(self, parser: argparse._ActionsContainer) -> None:\n        group: argparse._ArgumentGroup\n        if self.is_mutually_exclusive:\n            group = parser.add_mutually_exclusive_group(required=self.required)\n        else:\n            group = parser.add_argument_group(self.name)\n        for option in self.options:\n            option.add_to_group(group)\n\n    def add_to_group(self, group: argparse._ArgumentGroup) -> None:\n        self.add_to_parser(group)\n\n\ndef split_lists(separator: str) -> type[argparse.Action]:\n    \"\"\"\n    Works the same as `append` except each argument\n    is considered a `separator`-separated list.\n    \"\"\"\n\n    class SplitList(argparse.Action):\n        def __call__(\n            self,\n            parser: argparse.ArgumentParser,\n            args: argparse.Namespace,\n            values: Any,\n            option_string: str | None = None,\n        ) -> None:\n            if not isinstance(values, str):\n                return\n            split = getattr(args, self.dest) or []\n            split.extend(value.strip() for value in values.split(separator) if value.strip())\n            setattr(args, self.dest, split)\n\n    return SplitList\n\n\ndef from_splitted_env(name: str, separator: str) -> list[str] | None:\n    \"\"\"\n    Parse a `separator`-separated list from a `name` environment variable if present.\n    \"\"\"\n    value = os.getenv(name)\n    if not value:\n        return None\n    return [v.strip() for v in value.split(separator) if v.strip()] or None\n\n\nverbose_option = ArgumentGroup(\"Verbosity options\", is_mutually_exclusive=True)\nverbose_option.add_argument(\n    \"-v\",\n    \"--verbose\",\n    action=\"count\",\n    default=0,\n    help=\"Use `-v` for detailed output and `-vv` for more detailed\",\n)\nverbose_option.add_argument(\"-q\", \"--quiet\", action=\"store_const\", const=-1, dest=\"verbose\", help=\"Suppress output\")\n\nno_cache_option = Option(\n    \"--no-cache\",\n    action=\"store_true\",\n    default=os.getenv(\"PDM_NO_CACHE\"),\n    help=\"Disable the cache for the current command. [env var: PDM_NO_CACHE]\",\n)\n\ndry_run_option = Option(\n    \"--dry-run\",\n    action=\"store_true\",\n    default=False,\n    help=\"Show the difference only and don't perform any action\",\n)\n\n\nlockfile_option = Option(\n    \"-L\",\n    \"--lockfile\",\n    default=os.getenv(\"PDM_LOCKFILE\"),\n    help=\"Specify another lockfile path. Default: pdm.lock. [env var: PDM_LOCKFILE]\",\n)\n\n\n@Option(\n    \"--frozen-lockfile\",\n    \"--no-lock\",\n    nargs=0,\n    help=\"Don't try to create or update the lockfile. [env var: PDM_FROZEN_LOCKFILE]\",\n)\ndef frozen_lockfile_option(\n    project: Project,\n    namespace: argparse.Namespace,\n    values: str | Sequence[Any] | None,\n    option_string: str | None = None,\n) -> None:\n    if option_string == \"--no-lock\":\n        project.core.ui.warn(\"--no-lock is deprecated, use --frozen-lockfile instead.\")\n    project.enable_write_lockfile = False  # type: ignore[has-type]\n\n\n@Option(\n    \"--pep582\",\n    const=\"AUTO\",\n    metavar=\"SHELL\",\n    nargs=\"?\",\n    help=\"Print the command line to be eval'd by the shell for PEP 582\",\n)\ndef pep582_option(\n    project: Project,\n    namespace: argparse.Namespace,\n    values: str | Sequence[Any] | None,\n    option_string: str | None = None,\n) -> None:\n    from pdm.cli.actions import print_pep582_command\n\n    print_pep582_command(project, cast(str, values))\n    sys.exit(0)\n\n\ninstall_group = ArgumentGroup(\"Install options\")\ninstall_group.add_argument(\n    \"--no-editable\",\n    action=\"store_true\",\n    default=bool(os.getenv(\"PDM_NO_EDITABLE\")),\n    dest=\"no_editable\",\n    help=\"Install non-editable versions for all packages. [env var: PDM_NO_EDITABLE]\",\n)\ninstall_group.add_argument(\n    \"--no-self\",\n    action=\"store_true\",\n    default=bool(os.getenv(\"PDM_NO_SELF\")),\n    dest=\"no_self\",\n    help=\"Don't install the project itself. [env var: PDM_NO_SELF]\",\n)\ninstall_group.add_argument(\"--fail-fast\", \"-x\", action=\"store_true\", help=\"Abort on first installation error\")\n\n\n@Option(\n    \"--no-isolation\",\n    dest=\"build_isolation\",\n    nargs=0,\n    help=\"Disable isolation when building a source distribution that follows PEP 517, \"\n    \"as in: build dependencies specified by PEP 518 must be already installed if this option is used.\",\n)\ndef no_isolation_option(\n    project: Project,\n    namespace: argparse.Namespace,\n    values: str | Sequence[Any] | None,\n    option_string: str | None = None,\n) -> None:\n    project.core.state.build_isolation = False\n\n\ninstall_group.options.append(no_isolation_option)\n\ngroups_group = ArgumentGroup(\"Dependencies Selection\")\ngroups_group.add_argument(\n    \"-G\",\n    \"--group\",\n    \"--with\",\n    dest=\"groups\",\n    metavar=\"GROUP\",\n    action=split_lists(\",\"),\n    help=\"Select group of optional-dependencies separated by comma \"\n    \"or dependency-groups (with `-d`). Can be supplied multiple times, \"\n    'use \":all\" to include all groups under the same species.',\n    default=[],\n)\ngroups_group.add_argument(\n    \"--without\",\n    dest=\"excluded_groups\",\n    metavar=\"\",\n    action=split_lists(\",\"),\n    help=\"Exclude groups of optional-dependencies or dependency-groups\",\n    default=[],\n)\ngroups_group.add_argument(\n    \"--no-default\",\n    dest=\"default\",\n    action=\"store_false\",\n    default=True,\n    help=\"Don't include dependencies from the default group\",\n)\n\ndev_group = ArgumentGroup(\"dev\", is_mutually_exclusive=True)\ndev_group.add_argument(\n    \"-d\",\n    \"--dev\",\n    default=None,\n    dest=\"dev\",\n    action=\"store_true\",\n    help=\"Select dev dependencies\",\n)\ndev_group.add_argument(\n    \"--prod\",\n    \"--production\",\n    dest=\"dev\",\n    action=\"store_false\",\n    help=\"Unselect dev dependencies\",\n)\ngroups_group.options.append(dev_group)\n\nsave_strategy_group = ArgumentGroup(\"Save Strategy\")\n_save_sub_group = ArgumentGroup(\"save_strategy\", is_mutually_exclusive=True)\n_save_sub_group.add_argument(\n    \"--save-compatible\",\n    action=\"store_const\",\n    dest=\"save_strategy\",\n    const=\"compatible\",\n    help=\"Save compatible version specifiers\",\n)\n_save_sub_group.add_argument(\n    \"--save-safe-compatible\",\n    action=\"store_const\",\n    dest=\"save_strategy\",\n    const=\"safe_compatible\",\n    help=\"Save safe compatible version specifiers\",\n)\n_save_sub_group.add_argument(\n    \"--save-wildcard\",\n    action=\"store_const\",\n    dest=\"save_strategy\",\n    const=\"wildcard\",\n    help=\"Save wildcard version specifiers\",\n)\n_save_sub_group.add_argument(\n    \"--save-exact\",\n    action=\"store_const\",\n    dest=\"save_strategy\",\n    const=\"exact\",\n    help=\"Save exact version specifiers\",\n)\n_save_sub_group.add_argument(\n    \"--save-minimum\",\n    action=\"store_const\",\n    dest=\"save_strategy\",\n    const=\"minimum\",\n    help=\"Save minimum version specifiers\",\n)\nsave_strategy_group.add_argument(_save_sub_group)\n\nskip_option = Option(\n    \"-k\",\n    \"--skip\",\n    dest=\"skip\",\n    action=split_lists(\",\"),\n    help=\"Skip some tasks and/or hooks by their comma-separated names.\"\n    \" Can be supplied multiple times.\"\n    ' Use \":all\" to skip all hooks.'\n    ' Use \":pre\" and \":post\" to skip all pre or post hooks.',\n    default=from_splitted_env(\"PDM_SKIP_HOOKS\", \",\"),\n)\n\nupdate_strategy_group = ArgumentGroup(\"Update Strategy\")\n_update_sub_group = ArgumentGroup(\"update_strategy\", is_mutually_exclusive=True)\n_update_sub_group.add_argument(\n    \"--update-reuse\",\n    action=\"store_const\",\n    dest=\"update_strategy\",\n    const=\"reuse\",\n    help=\"Reuse pinned versions already present in lock file if possible\",\n)\n_update_sub_group.add_argument(\n    \"--update-eager\",\n    action=\"store_const\",\n    dest=\"update_strategy\",\n    const=\"eager\",\n    help=\"Try to update the packages and their dependencies recursively\",\n)\n_update_sub_group.add_argument(\n    \"--update-all\",\n    action=\"store_const\",\n    dest=\"update_strategy\",\n    const=\"all\",\n    help=\"Update all dependencies and sub-dependencies\",\n)\n_update_sub_group.add_argument(\n    \"--update-reuse-installed\",\n    action=\"store_const\",\n    dest=\"update_strategy\",\n    const=\"reuse-installed\",\n    help=\"Reuse installed packages if possible\",\n)\nupdate_strategy_group.add_argument(_update_sub_group)\n\nproject_option = Option(\n    \"-p\",\n    \"--project\",\n    dest=\"project_path\",\n    help=\"Specify another path as the project root, which changes the base of pyproject.toml \"\n    \"and __pypackages__ [env var: PDM_PROJECT]\",\n    default=os.getenv(\"PDM_PROJECT\"),\n)\n\n\nglobal_option = Option(\n    \"-g\",\n    \"--global\",\n    dest=\"global_project\",\n    action=\"store_true\",\n    help=\"Use the global project, supply the project root with `-p` option\",\n)\n\nclean_group = ArgumentGroup(\"clean\", is_mutually_exclusive=True)\nclean_group.add_argument(\"--clean\", action=\"store_true\", help=\"Clean packages not in the lockfile\")\nclean_group.add_argument(\n    \"--only-keep\", \"--clean-unselected\", action=\"store_true\", help=\"Only keep the selected packages\"\n)\n\npackages_group = ArgumentGroup(\"Package Arguments\")\npackages_group.add_argument(\n    \"-e\",\n    \"--editable\",\n    dest=\"editables\",\n    action=\"append\",\n    help=\"Specify editable packages\",\n    default=[],\n)\npackages_group.add_argument(\"packages\", nargs=\"*\", help=\"Specify packages\")\n\n\n@Option(\n    \"-I\",\n    \"--ignore-python\",\n    nargs=0,\n    help=\"Ignore the Python path saved in .pdm-python. [env var: PDM_IGNORE_SAVED_PYTHON]\",\n)\ndef ignore_python_option(\n    project: Project,\n    namespace: argparse.Namespace,\n    values: str | Sequence[Any] | None,\n    option_string: str | None = None,\n) -> None:\n    os.environ.update({\"PDM_IGNORE_SAVED_PYTHON\": \"1\"})\n\n\n@Option(\n    \"-n\",\n    \"--non-interactive\",\n    nargs=0,\n    dest=\"_non_interactive\",\n    help=\"Don't show interactive prompts but use defaults. [env var: PDM_NON_INTERACTIVE]\",\n)\ndef non_interactive_option(\n    project: Project,\n    namespace: argparse.Namespace,\n    values: str | Sequence[Any] | None,\n    option_string: str | None = None,\n) -> None:\n    os.environ.update({\"PDM_NON_INTERACTIVE\": \"1\"})\n\n\nprerelease_option = ArgumentGroup(\"prerelease\", is_mutually_exclusive=True)\nprerelease_option.add_argument(\n    \"--pre\",\n    \"--prerelease\",\n    action=\"store_true\",\n    dest=\"prerelease\",\n    default=None,\n    help=\"Allow prereleases to be pinned\",\n)\nprerelease_option.add_argument(\n    \"--stable\", action=\"store_false\", dest=\"prerelease\", help=\"Only allow stable versions to be pinned\"\n)\nunconstrained_option = Option(\n    \"-u\",\n    \"--unconstrained\",\n    action=\"store_true\",\n    default=False,\n    help=\"Ignore the version constraints in pyproject.toml and overwrite with new ones from the resolution result\",\n)\n\n\nvenv_option = Option(\n    \"--venv\",\n    dest=\"use_venv\",\n    metavar=\"NAME\",\n    nargs=\"?\",\n    const=\"in-project\",\n    help=\"Run the command in the virtual environment with the given key. [env var: PDM_IN_VENV]\",\n    default=os.getenv(\"PDM_IN_VENV\"),\n)\n\n\nlock_strategy_group = ArgumentGroup(\"Lock Strategy\")\nlock_strategy_group.add_argument(\n    \"--strategy\",\n    \"-S\",\n    dest=\"strategy_change\",\n    metavar=\"STRATEGY\",\n    action=split_lists(\",\"),\n    help=\"Specify lock strategy (cross_platform, static_urls, direct_minimal_versions, inherit_metadata). \"\n    \"Add 'no_' prefix to disable. Can be supplied multiple times or split by comma.\",\n)\nlock_strategy_group.add_argument(\n    \"--no-cross-platform\",\n    action=\"append_const\",\n    dest=\"strategy_change\",\n    const=\"no_cross_platform\",\n    help=\"[DEPRECATED] Only lock packages for the current platform\",\n)\nlock_strategy_group.add_argument(\n    \"--static-urls\",\n    action=\"append_const\",\n    dest=\"strategy_change\",\n    help=\"[DEPRECATED] Store static file URLs in the lockfile\",\n    const=\"static_urls\",\n)\nlock_strategy_group.add_argument(\n    \"--no-static-urls\",\n    action=\"append_const\",\n    dest=\"strategy_change\",\n    help=\"[DEPRECATED] Do not store static file URLs in the lockfile\",\n    const=\"no_static_urls\",\n)\n\nconfig_setting_option = Option(\n    \"--config-setting\",\n    \"-C\",\n    action=ExtendMapAction,\n    help=\"Pass options to the builder. Options with a value must be \"\n    'specified after \"=\": `--config-setting=key(=value)` '\n    \"or `-Ckey(=value)`\",\n)\ninstall_group.options.append(config_setting_option)\n\noverride_option = Option(\n    \"--override\",\n    default=[env] if (env := os.getenv(\"PDM_OVERRIDE\")) else None,\n    action=\"append\",\n    help=\"Use the constraint file in pip-requirements format for overriding. [env var: PDM_OVERRIDE] \"\n    \"This option can be used multiple times. \"\n    \"See https://pip.pypa.io/en/stable/user_guide/#constraints-files\",\n)\n"
  },
  {
    "path": "src/pdm/cli/templates/__init__.py",
    "content": "from __future__ import annotations\n\nimport importlib.resources\nimport os\nimport re\nimport shutil\nimport subprocess\nimport tempfile\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any\n\nfrom pdm.exceptions import PdmException\nfrom pdm.utils import normalize_name\n\nif TYPE_CHECKING:\n    from importlib.resources.abc import Traversable\n    from typing import Callable, TypeVar\n\n    ST = TypeVar(\"ST\", Traversable, Path)\n\nTEMPLATE_PACKAGE = \"pdm.cli.templates\"\nBUILTIN_TEMPLATES = [\"default\", \"minimal\"]\n\n\nclass ProjectTemplate:\n    _path: Path\n\n    def __init__(self, path_or_url: str | None) -> None:\n        self.template = path_or_url or \"default\"\n\n    def __enter__(self) -> ProjectTemplate:\n        self._path = Path(tempfile.mkdtemp(suffix=\"-template\", prefix=\"pdm-\"))\n        self.prepare_template()\n        return self\n\n    def __exit__(self, *args: Any) -> None:\n        shutil.rmtree(self._path, ignore_errors=True)\n\n    def generate(self, target_path: Path, metadata: dict[str, Any], overwrite: bool = False) -> None:\n        from pdm.compat import tomllib\n\n        def replace_all(path: str, old: str, new: str) -> None:\n            with open(path, encoding=encoding) as fp:\n                content = fp.read()\n            content = re.sub(rf\"\\b{old}\\b\", new, content)\n            with open(path, \"w\", encoding=encoding) as fp:\n                fp.write(content)\n\n        if metadata.get(\"project\", {}).get(\"name\"):\n            try:\n                with open(self._path / \"pyproject.toml\", \"rb\") as fp:\n                    pyproject = tomllib.load(fp)\n            except FileNotFoundError:\n                raise PdmException(\"Template pyproject.toml not found\") from None\n            new_name = metadata[\"project\"][\"name\"]\n            new_import_name = normalize_name(new_name).replace(\"-\", \"_\")\n            try:\n                original_name = pyproject[\"project\"][\"name\"]\n            except KeyError:\n                raise PdmException(\"Template pyproject.toml is not PEP-621 compliant\") from None\n            import_name = normalize_name(original_name).replace(\"-\", \"_\")\n            encoding = \"utf-8\"\n            for root, dirs, filenames in os.walk(self._path):\n                for i, d in enumerate(dirs):\n                    if d == import_name:\n                        os.rename(os.path.join(root, d), os.path.join(root, new_import_name))\n                        dirs[i] = new_import_name\n                for f in filenames:\n                    if f.endswith(\".py\"):\n                        replace_all(os.path.join(root, f), import_name, new_import_name)\n                        if f == import_name + \".py\":\n                            os.rename(os.path.join(root, f), os.path.join(root, new_import_name + \".py\"))\n                    elif f.endswith((\".md\", \".rst\")):\n                        replace_all(os.path.join(root, f), original_name, new_name)\n                        replace_all(os.path.join(root, f), import_name, new_import_name)\n                    elif Path(root) == self._path and f == \"pyproject.toml\":\n                        replace_all(os.path.join(root, f), original_name, new_name)\n                        replace_all(os.path.join(root, f), import_name, new_import_name)\n\n        target_path.mkdir(exist_ok=True, parents=True)\n        self.mirror(self._path, target_path, [self._path / \"pyproject.toml\"], overwrite=overwrite)\n        self._generate_pyproject(target_path / \"pyproject.toml\", metadata)\n\n    def prepare_template(self) -> None:\n        if self.template in BUILTIN_TEMPLATES:\n            self._prepare_package_template(f\"{TEMPLATE_PACKAGE}.{self.template}\")\n        elif \"://\" in self.template or self.template.startswith(\"git@\"):\n            self._prepare_git_template(self.template)\n        elif os.path.exists(self.template):\n            self._prepare_local_template(self.template)\n        else:  # template name\n            template = f\"https://github.com/pdm-project/template-{self.template}\"\n            self._prepare_git_template(template)\n\n    @staticmethod\n    def mirror(\n        src: ST,\n        dst: Path,\n        skip: list[ST] | None = None,\n        copyfunc: Callable[[ST, Path], Any] = shutil.copyfile,  # type: ignore[assignment]\n        *,\n        overwrite: bool = False,\n    ) -> None:\n        if skip and src in skip:\n            return\n        if src.is_dir():\n            dst.mkdir(exist_ok=True)\n            for child in src.iterdir():\n                ProjectTemplate.mirror(child, dst / child.name, skip, copyfunc)\n        elif src.name.endswith(\".pyc\"):\n            return\n        elif overwrite or not dst.exists():\n            copyfunc(src, dst)\n\n    @staticmethod\n    def _copy_package_file(src: Traversable, dst: Path) -> Path:\n        with importlib.resources.as_file(src) as f:\n            return shutil.copyfile(f, dst)\n\n    def _generate_pyproject(self, path: Path, metadata: dict[str, Any]) -> None:\n        import tomlkit\n\n        from pdm.cli.utils import merge_dictionary\n\n        try:\n            with open(path, encoding=\"utf-8\") as fp:\n                content = tomlkit.load(fp)\n        except FileNotFoundError:\n            content = tomlkit.document()\n        try:\n            with open(self._path / \"pyproject.toml\", encoding=\"utf-8\") as fp:\n                template_content = tomlkit.load(fp)\n        except FileNotFoundError:\n            template_content = tomlkit.document()\n\n        merge_dictionary(content, template_content)\n        if \"version\" in content.get(\"project\", {}).get(\"dynamic\", []):\n            metadata[\"project\"].pop(\"version\", None)\n        merge_dictionary(content, metadata)\n        if \"build-system\" in metadata:\n            content[\"build-system\"] = metadata[\"build-system\"]\n        else:\n            content.pop(\"build-system\", None)\n        with open(path, \"w\", encoding=\"utf-8\") as fp:\n            fp.write(tomlkit.dumps(content))\n\n    def _prepare_package_template(self, import_name: str) -> None:\n        files = importlib.resources.files(import_name)\n\n        self.mirror(files, self._path, skip=[files / \"__init__.py\"], copyfunc=self._copy_package_file)\n\n    def _prepare_git_template(self, url: str) -> None:\n        left, amp, right = url.rpartition(\"@\")\n        if left != \"git\" and amp:\n            extra_args = [f\"--branch={right}\"]\n            url = left\n        else:\n            extra_args = []\n        git_command = [\"git\", \"clone\", \"--recursive\", \"--depth=1\", *extra_args, url, self._path.as_posix()]\n        result = subprocess.run(git_command, capture_output=True, text=True)\n        if result.returncode != 0:\n            raise PdmException(f\"Failed to clone template from git repository {url}: {result.stderr}\")\n        shutil.rmtree(self._path / \".git\", ignore_errors=True)\n\n    def _prepare_local_template(self, path: str) -> None:\n        src = Path(path)\n\n        self.mirror(src, self._path, skip=[src / \".git\", src / \".svn\", src / \".hg\"])\n"
  },
  {
    "path": "src/pdm/cli/templates/default/.gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[codz]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#   Usually these files are written by a python script from a template\n#   before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py.cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n# Pipfile.lock\n\n# UV\n#   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n# uv.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n# poetry.lock\n# poetry.toml\n\n# pdm\n#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#   pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.\n#   https://pdm-project.org/en/latest/usage/project/#working-with-version-control\n# pdm.lock\n# pdm.toml\n.pdm-python\n.pdm-build/\n\n# pixi\n#   Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.\n# pixi.lock\n#   Pixi creates a virtual environment in the .pixi directory, just like venv module creates one\n#   in the .venv directory. It is recommended not to include this directory in version control.\n.pixi\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# Redis\n*.rdb\n*.aof\n*.pid\n\n# RabbitMQ\nmnesia/\nrabbitmq/\nrabbitmq-data/\n\n# ActiveMQ\nactivemq-data/\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.envrc\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#   JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#   be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#   and can be added to the global gitignore or merged into this file.  For a more nuclear\n#   option (not recommended) you can uncomment the following to ignore the entire idea folder.\n# .idea/\n\n# Abstra\n#   Abstra is an AI-powered process automation framework.\n#   Ignore directories containing user credentials, local state, and settings.\n#   Learn more at https://abstra.io/docs\n.abstra/\n\n# Visual Studio Code\n#   Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore\n#   that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore\n#   and can be added to the global gitignore or merged into this file. However, if you prefer,\n#   you could uncomment the following to ignore the entire vscode folder\n# .vscode/\n\n# Ruff stuff:\n.ruff_cache/\n\n# PyPI configuration file\n.pypirc\n\n# Marimo\nmarimo/_static/\nmarimo/_lsp/\n__marimo__/\n\n# Streamlit\n.streamlit/secrets.toml\n"
  },
  {
    "path": "src/pdm/cli/templates/default/README.md",
    "content": "# example-package\n"
  },
  {
    "path": "src/pdm/cli/templates/default/__init__.py",
    "content": ""
  },
  {
    "path": "src/pdm/cli/templates/default/pyproject.toml",
    "content": "[project]\nname = \"example-package\"\nversion = \"0.1.0\"\ndescription = \"Default template for PDM package\"\nauthors = []\ndependencies = []\nrequires-python = \">=3.9\"\nreadme = \"README.md\"\nlicense = {text = \"MIT\"}\n\n[build-system]\nrequires = [\"pdm-backend\"]\nbuild-backend = \"pdm.backend\"\n"
  },
  {
    "path": "src/pdm/cli/templates/default/src/example_package/__init__.py",
    "content": ""
  },
  {
    "path": "src/pdm/cli/templates/default/tests/__init__.py",
    "content": ""
  },
  {
    "path": "src/pdm/cli/templates/minimal/.gitignore",
    "content": "*.py[codz]\n__pycache__/\n.mypy_cache/\n.pytest_cache/\n.ruff_cache/\n.pdm-python\n"
  },
  {
    "path": "src/pdm/cli/templates/minimal/__init__.py",
    "content": ""
  },
  {
    "path": "src/pdm/cli/templates/minimal/pyproject.toml",
    "content": "[project]\nname = \"example-package\"\nversion = \"0.1.0\"\ndescription = \"Default template for PDM package\"\nauthors = []\ndependencies = []\nrequires-python = \">=3.9\"\nreadme = \"README.md\"\nlicense = {text = \"MIT\"}\n\n[build-system]\nrequires = [\"pdm-backend\"]\nbuild-backend = \"pdm.backend\"\n"
  },
  {
    "path": "src/pdm/cli/utils.py",
    "content": "from __future__ import annotations\n\nimport argparse\nimport dataclasses as dc\nimport json\nimport os\nimport re\nimport sys\nfrom collections import OrderedDict\nfrom fnmatch import fnmatch\nfrom gettext import gettext as _\nfrom json import dumps\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, Callable, Iterable, Mapping, MutableMapping, cast, no_type_check\n\nfrom packaging.specifiers import SpecifierSet\nfrom resolvelib.structs import DirectedGraph\nfrom rich.tree import Tree\n\nfrom pdm import termui\nfrom pdm.exceptions import PdmArgumentError, ProjectError\nfrom pdm.models.specifiers import PySpecSet, get_specifier\nfrom pdm.utils import comparable_version, is_path_relative_to, normalize_name, url_to_path\n\nif TYPE_CHECKING:\n    from argparse import Action, _ArgumentGroup\n\n    from packaging.version import Version\n    from resolvelib.resolvers import RequirementInformation, ResolutionImpossible\n\n    from pdm.compat import Distribution\n    from pdm.compat import importlib_metadata as im\n    from pdm.models.candidates import Candidate\n    from pdm.models.markers import EnvSpec\n    from pdm.models.requirements import Requirement\n    from pdm.project import Project\n\n\nclass PdmFormatter(argparse.RawDescriptionHelpFormatter):\n    def start_section(self, heading: str | None) -> None:\n        return super().start_section(termui.style(heading.title() if heading else \"\", style=\"warning\"))\n\n    def _format_usage(\n        self,\n        usage: str | None,\n        actions: Iterable[Action],\n        groups: Iterable[_ArgumentGroup],\n        prefix: str | None,\n    ) -> str:\n        if prefix is None:\n            prefix = \"Usage: \"\n        result = super()._format_usage(usage, actions, groups, prefix)  # type: ignore[arg-type]\n        # Remove continuous spaces\n        result = re.sub(r\" +\", \" \", result)\n        if prefix:\n            return result.replace(prefix, termui.style(prefix, style=\"warning\"))\n        return result\n\n    def _format_action(self, action: Action) -> str:\n        # determine the required width and the entry label\n        help_position = min(self._action_max_length + 2, self._max_help_position)\n        help_width = max(self._width - help_position, 11)\n        action_width = help_position - self._current_indent - 2\n        action_header = self._format_action_invocation(action)\n\n        # no help; start on same line and add a final newline\n        if not action.help:\n            tup = \"\", self._current_indent, action_header\n            action_header = \"{0:>{1}}{2}\\n\".format(*tup)\n\n        # short action name; start on the same line and pad two spaces\n        elif len(action_header) <= action_width:\n            tup = \"\", self._current_indent, action_header, action_width  # type: ignore[assignment]\n            action_header = \"{0:>{1}}{2:<{3}}  \".format(*tup)\n            indent_first = 0\n\n        # long action name; start on the next line\n        else:\n            tup = \"\", self._current_indent, action_header\n            action_header = \"{0:>{1}}{2}\\n\".format(*tup)\n            indent_first = help_position\n\n        # Special format for empty action_header\n        # - No extra indent block\n        # - Help info in the same indent level as subactions\n        if not action_header.strip():\n            action_header = \"\"\n            help_position = self._current_indent\n            indent_first = self._current_indent\n\n        # collect the pieces of the action help\n        parts = [termui.style(action_header, style=\"primary\")]\n\n        # if there was help for the action, add lines of help text\n        if action.help:\n            help_text = self._expand_help(action)\n            help_lines = []\n            for help_line in help_text.split(\"\\n\"):\n                help_lines += self._split_lines(help_line, help_width)\n            parts.append(\"{:>{}}{}\\n\".format(\"\", indent_first, help_lines[0]))\n            for line in help_lines[1:]:\n                parts.append(\"{:>{}}{}\\n\".format(\"\", help_position, line))\n\n        # or add a newline if the description doesn't end with one\n        elif not action_header.endswith(\"\\n\"):\n            if action_header:\n                parts.append(\"\\n\")\n\n        # cancel out extra indent when action_header is empty\n        if not action_header:\n            self._dedent()\n        # if there are any sub-actions, add their help as well\n        for subaction in self._iter_indented_subactions(action):\n            parts.append(self._format_action(subaction))\n        # cancel out extra dedent when action_header is empty\n        if not action_header:\n            self._indent()\n\n        # return a single string\n        return self._join_parts(parts)\n\n\nclass ArgumentParser(argparse.ArgumentParser):\n    \"\"\"A standard argument parser but with title-cased help.\"\"\"\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        if sys.version_info >= (3, 14):\n            kwargs[\"color\"] = False\n        kwargs[\"formatter_class\"] = PdmFormatter\n        kwargs[\"add_help\"] = False\n        super().__init__(*args, **kwargs)\n        self.add_argument(\n            \"-h\", \"--help\", action=\"help\", default=argparse.SUPPRESS, help=\"Show this help message and exit.\"\n        )\n        self._optionals.title = \"options\"\n\n    def parse_known_args(self, args: Any = None, namespace: Any = None) -> Any:\n        args, argv = super().parse_known_args(args, namespace)\n        if argv:\n            msg = _(\"unrecognized arguments: %s\")\n            self.error(msg % \" \".join(argv))\n        return args, argv\n\n\ndef format_similar_command(root_command: str, commands: list[str], script_commands: list[str]) -> str:\n    from difflib import get_close_matches\n\n    similar_commands = get_close_matches(root_command, commands)\n    similar_script_commands = get_close_matches(root_command, script_commands)\n    commands_text = \"\\n\".join([f\"  - {cmd}\" for cmd in similar_commands])\n    script_commands_text = \"\\n\".join([f\"  - {cmd}\" for cmd in similar_script_commands])\n    message = f\"[red]Command not found: {root_command}[/]\"\n    if commands_text:\n        message += f\"\"\"\n[green]Did you mean one of these commands?\n{commands_text}[/]\"\"\"\n\n    if script_commands_text:\n        message += f\"\"\"\n[yellow]{\"Or\" if commands_text else \"Did you mean\"} one of these script commands?\n{script_commands_text}[/]\"\"\"\n    return message\n\n\nclass ErrorArgumentParser(ArgumentParser):\n    \"\"\"A subclass of argparse.ArgumentParser that raises\n    parsing error rather than exiting.\n\n    This does the same as passing exit_on_error=False on Python 3.9+\n    \"\"\"\n\n    def _parse_known_args(\n        self, arg_strings: list[str], namespace: argparse.Namespace, *args: Any, **kwargs: Any\n    ) -> tuple[argparse.Namespace, list[str]]:\n        try:\n            return super()._parse_known_args(arg_strings, namespace, *args, **kwargs)\n        except argparse.ArgumentError as e:\n            # We raise a dedicated error to avoid being caught by the caller\n            raise PdmArgumentError(e) from e\n\n\n@dc.dataclass(frozen=True)\nclass PackageNode:\n    \"\"\"An internal class for the convenience of dependency graph building.\"\"\"\n\n    name: str = dc.field(hash=True, compare=True)\n    version: str | None = dc.field(compare=False)\n    requirements: dict[str, Requirement] = dc.field(compare=False)\n\n    def __repr__(self) -> str:\n        return f\"<Package {self.name}=={self.version}>\"\n\n\ndef build_dependency_graph(\n    working_set: Mapping[str, im.Distribution],\n    env_spec: EnvSpec,\n    selected: set[str] | None = None,\n    include_sub: bool = True,\n) -> DirectedGraph:\n    \"\"\"Build a dependency graph from locked result.\"\"\"\n    graph: DirectedGraph[PackageNode | None] = DirectedGraph()\n    graph.add(None)  # sentinel parent of top nodes.\n    node_with_extras: set[str] = set()\n\n    def add_package(key: str, dist: Distribution | None) -> PackageNode:\n        from pdm.models.requirements import filter_requirements_with_extras, strip_extras\n\n        name, extras = strip_extras(key)\n        extras = extras or ()\n        reqs: dict[str, Requirement] = {}\n        if dist:\n            requirements = filter_requirements_with_extras(dist.requires or [], extras, include_default=True)\n            for req in requirements:\n                if not req.marker or req.marker.matches(env_spec):\n                    reqs[req.identify()] = req\n            version: str | None = dist.version\n        else:\n            version = None\n\n        node = PackageNode(key, version, reqs)\n        if node not in graph:\n            if extras:\n                node_with_extras.add(name)\n            graph.add(node)\n            if include_sub:\n                for k in reqs:\n                    child = add_package(k, working_set.get(strip_extras(k)[0]))\n                    graph.connect(node, child)\n\n        return node\n\n    selected_map: dict[str, str] = {}\n    for key in selected or ():\n        name = key.split(\"[\")[0]\n        if len(key) >= len(selected_map.get(name, \"\")):\n            # Ensure key with extras remains\n            selected_map[name] = key\n    for k, dist in working_set.items():\n        if selected is not None:\n            name = k.split(\"[\")[0]\n            if name not in selected_map:\n                continue\n            k = selected_map[name]\n        add_package(k, dist)\n    for node in list(graph):\n        if node is not None and not list(graph.iter_parents(node)):\n            # Top requirements\n            if node.name in node_with_extras:\n                # Already included in package[extra], no need to keep the top level\n                # non-extra package.\n                graph.remove(node)\n            else:\n                graph.connect(None, node)\n    return graph\n\n\ndef specifier_from_requirement(requirement: Requirement) -> str:\n    return str(requirement.specifier) or \"Any\"\n\n\ndef add_package_to_tree(\n    root: Tree,\n    graph: DirectedGraph,\n    package: PackageNode,\n    required: list[str],\n    visited: frozenset[str] = frozenset(),\n) -> None:\n    \"\"\"Format one package.\n\n    :param graph: the dependency graph\n    :param package: the package instance\n    :param required: the version required by its parent\n    :param visited: the visited package collection\n    \"\"\"\n    version = (\n        \"[error][ not installed ][/]\"\n        if not package.version\n        else f\"[error]{package.version}[/]\"\n        if required\n        and not any(s in (\"Any\", \"This project\") or SpecifierSet(s).contains(package.version) for s in required)\n        else f\"[warning]{package.version}[/]\"\n    )\n    # escape deps with extras\n    name = package.name.replace(\"[\", r\"\\[\") if \"[\" in package.name else package.name\n    if package.name in visited:\n        version = r\"[error]\\[circular][/]\"\n    req_str = f\"[ required: {'&&'.join(required)} ]\" if required else \"[ Not required ]\"\n    node = root.add(f\"[req]{name}[/] {version} {req_str}\")\n    if package.name in visited:\n        return\n    children = sorted(graph.iter_children(package), key=lambda p: p.name)\n    for child in children:\n        required = [specifier_from_requirement(package.requirements[child.name])]\n        add_package_to_tree(node, graph, child, required, visited | {package.name})\n\n\ndef add_package_to_reverse_tree(\n    root: Tree,\n    graph: DirectedGraph,\n    package: PackageNode,\n    child: PackageNode | None = None,\n    requires: str = \"\",\n    visited: frozenset[str] = frozenset(),\n) -> None:\n    \"\"\"Format one package for output reverse dependency graph.\"\"\"\n    version = \"[error][ not installed ][/]\" if not package.version else f\"[warning]{package.version}[/]\"\n    if package.name in visited:\n        version = r\"[error]\\[circular][/]\"\n    requires = (\n        f\"[ requires: [error]{requires}[/] ]\"\n        if requires not in (\"Any\", \"\")\n        and child\n        and child.version\n        and not SpecifierSet(requires).contains(child.version)\n        else \"\"\n        if not requires\n        else f\"[ requires: {requires} ]\"\n    )\n    name = package.name.replace(\"[\", r\"\\[\") if \"[\" in package.name else package.name\n    node = root.add(f\"[req]{name}[/] {version} {requires}\")\n\n    if package.name in visited:\n        return\n    parents: list[PackageNode] = sorted(filter(None, graph.iter_parents(package)), key=lambda p: p.name)\n    for parent in parents:\n        requires = specifier_from_requirement(parent.requirements[package.name])\n        add_package_to_reverse_tree(node, graph, parent, package, requires, visited=visited | {package.name})\n    return\n\n\ndef package_is_project(package: PackageNode, project: Project) -> bool:\n    return project.is_distribution and package.name == normalize_name(project.name)\n\n\ndef _format_forward_dependency_graph(\n    project: Project, graph: DirectedGraph[PackageNode | None], patterns: list[str]\n) -> Tree:\n    \"\"\"Format dependency graph for output.\"\"\"\n    root = Tree(\"Dependencies\", hide_root=True)\n\n    def find_package_to_add(package: PackageNode) -> PackageNode | None:\n        if not patterns:\n            return package\n        to_check = [package]\n        while to_check:\n            package = to_check.pop(0)\n            if package and package_match_patterns(package, patterns):\n                return package\n            to_check.extend(graph.iter_children(package))\n        return None\n\n    all_dependencies = {\n        r.identify(): r\n        for deps in project.all_dependencies.values()\n        for r in deps\n        if not r.marker or r.marker.matches(project.environment.spec)\n    }\n    top_level_dependencies = {find_package_to_add(node) for node in graph.iter_children(None) if node}\n    for package in sorted((node for node in top_level_dependencies if node), key=lambda p: p.name):\n        required: set[str] = set()\n        for parent in graph.iter_parents(package):\n            if parent:\n                r = specifier_from_requirement(parent.requirements[package.name])\n            elif package.name in all_dependencies:\n                r = specifier_from_requirement(all_dependencies[package.name])\n            elif package_is_project(package, project):\n                r = \"This project\"\n            else:\n                continue\n            required.add(r)\n        add_package_to_tree(root, graph, package, sorted(required))\n    return root\n\n\ndef _format_reverse_dependency_graph(\n    project: Project, graph: DirectedGraph[PackageNode | None], patterns: list[str]\n) -> Tree:\n    \"\"\"Format reverse dependency graph for output.\"\"\"\n\n    def find_package_to_add(package: PackageNode) -> PackageNode | None:\n        if not patterns:\n            return package\n        to_check = [package]\n        while to_check:\n            package = to_check.pop(0)\n            if package and package_match_patterns(package, patterns):\n                return package\n            to_check.extend(graph.iter_parents(package))\n        return None\n\n    root = Tree(\"Dependencies\", hide_root=True)\n    leaf_nodes = {find_package_to_add(node) for node in graph if not list(graph.iter_children(node)) and node}\n    for package in sorted((node for node in leaf_nodes if node), key=lambda p: p.name):\n        if not package:\n            continue\n        add_package_to_reverse_tree(root, graph, package)\n    return root\n\n\ndef build_forward_dependency_json_subtree(\n    root: PackageNode,\n    project: Project,\n    graph: DirectedGraph[PackageNode | None],\n    required_by: PackageNode | None = None,\n    visited: frozenset[str] = frozenset(),\n) -> dict:\n    required: set[str] = set()\n    all_dependencies = {\n        r.identify(): r\n        for deps in project.all_dependencies.values()\n        for r in deps\n        if not r.marker or r.marker.matches(project.environment.spec)\n    }\n    for parent in graph.iter_parents(root):\n        if parent:\n            r = specifier_from_requirement(parent.requirements[root.name])\n        elif not package_is_project(root, project):\n            requirements = required_by.requirements if required_by else all_dependencies\n            if root.name in requirements:\n                r = specifier_from_requirement(requirements[root.name])\n            else:\n                continue\n        else:\n            r = \"This project\"\n        required.add(r)\n\n    children = graph.iter_children(root) if root.name not in visited else []\n\n    return OrderedDict(\n        package=root.name,\n        version=root.version,\n        required=\"&&\".join(sorted(required)),\n        dependencies=sorted(\n            (\n                build_forward_dependency_json_subtree(p, project, graph, root, visited | {root.name})\n                for p in children\n                if p\n            ),\n            key=lambda d: d[\"package\"],\n        ),\n    )\n\n\ndef build_reverse_dependency_json_subtree(\n    root: PackageNode,\n    project: Project,\n    graph: DirectedGraph[PackageNode | None],\n    requires: PackageNode | None = None,\n    visited: frozenset[str] = frozenset(),\n) -> dict:\n    parents = graph.iter_parents(root) if root.name not in visited else []\n    return OrderedDict(\n        package=root.name,\n        version=root.version,\n        requires=specifier_from_requirement(root.requirements[requires.name]) if requires else None,\n        dependents=sorted(\n            (\n                build_reverse_dependency_json_subtree(p, project, graph, root, visited | {root.name})\n                for p in parents\n                if p\n            ),\n            key=lambda d: d[\"package\"],\n        ),\n    )\n\n\ndef package_match_patterns(package: PackageNode, patterns: list[str]) -> bool:\n    return not patterns or any(fnmatch(package.name, pattern) for pattern in patterns)\n\n\ndef build_dependency_json_tree(\n    project: Project, graph: DirectedGraph[PackageNode | None], reverse: bool, patterns: list[str]\n) -> list[dict]:\n    def find_package_to_add(package: PackageNode) -> PackageNode | None:\n        if not patterns:\n            return package\n        to_check = [package]\n        while to_check:\n            package = to_check.pop(0)\n            if package and package_match_patterns(package, patterns):\n                return package\n            if reverse:\n                to_check.extend(graph.iter_parents(package))\n            else:\n                to_check.extend(graph.iter_children(package))\n        return None\n\n    top_level_packages: Iterable[PackageNode | None]\n    if reverse:\n        top_level_packages = filter(lambda n: not list(graph.iter_children(n)), graph)  # leaf nodes\n        build_dependency_json_subtree: Callable = build_reverse_dependency_json_subtree\n    else:\n        top_level_packages = graph.iter_children(None)  # root nodes\n        build_dependency_json_subtree = build_forward_dependency_json_subtree\n    top_level_packages = {find_package_to_add(node) for node in top_level_packages if node}\n    return [\n        build_dependency_json_subtree(p, project, graph)\n        for p in sorted((node for node in top_level_packages if node), key=lambda p: p.name)\n    ]\n\n\ndef show_dependency_graph(\n    project: Project,\n    graph: DirectedGraph[PackageNode | None],\n    reverse: bool = False,\n    json: bool = False,\n    patterns: list[str] | None = None,\n) -> None:\n    echo = project.core.ui.echo\n    if patterns is None:\n        patterns = []\n    if json:\n        echo(\n            dumps(\n                build_dependency_json_tree(project, graph, reverse, patterns),\n                indent=2,\n            )\n        )\n        return\n\n    if reverse:\n        tree = _format_reverse_dependency_graph(project, graph, patterns)\n    else:\n        tree = _format_forward_dependency_graph(project, graph, patterns)\n    echo(tree)\n\n\ndef save_version_specifiers(\n    requirements: Iterable[Requirement],\n    resolved: dict[str, list[Candidate]],\n    save_strategy: str,\n) -> None:\n    \"\"\"Rewrite the version specifiers according to the resolved result and save strategy\n\n    :param requirements: the requirements to be updated\n    :param resolved: the resolved mapping\n    :param save_strategy: compatible/safe_compatible/wildcard/exact\n    \"\"\"\n\n    def candidate_version(candidates: list[Candidate]) -> Version | None:\n        if len(candidates) > 1:\n            return None\n        c = candidates[0]\n        assert c.version is not None\n        return comparable_version(c.version)\n\n    for r in requirements:\n        name = r.identify()\n        if r.is_named and not r.specifier and name in resolved:\n            version = candidate_version(resolved[name])\n            if version is None:\n                continue\n            if save_strategy == \"exact\":\n                r.specifier = get_specifier(f\"=={version}\")\n            elif save_strategy in [\"compatible\", \"safe_compatible\"]:\n                if version.is_prerelease or version.is_devrelease:\n                    r.specifier = get_specifier(f\">={version},<{version.major + 1}\")\n                else:\n                    if save_strategy == \"compatible\":\n                        r.specifier = get_specifier(f\"~={version.major}.{version.minor}\")\n                    else:\n                        r.specifier = get_specifier(f\"~={version}\")\n            elif save_strategy == \"minimum\":\n                r.specifier = get_specifier(f\">={version}\")\n\n\ndef check_project_file(project: Project) -> None:\n    \"\"\"Check the existence of the project file and throws an error on failure.\"\"\"\n    if not project.pyproject.is_valid:\n        raise ProjectError(\n            \"The pyproject.toml has not been initialized yet. You can do this by running [success]`pdm init`[/].\"\n        ) from None\n\n\ndef find_importable_files(project: Project) -> Iterable[tuple[str, Path]]:\n    \"\"\"Find all possible files that can be imported\"\"\"\n    from pdm.formats import FORMATS\n\n    for filename in (\"Pipfile\", \"pyproject.toml\", \"requirements.in\", \"requirements.txt\", \"setup.py\", \"setup.cfg\"):\n        project_file = project.root / filename\n        if not project_file.exists():\n            continue\n        for key, module in FORMATS.items():\n            if module.check_fingerprint(project, project_file.as_posix()):\n                yield key, project_file\n\n\n@no_type_check\ndef set_env_in_reg(env_name: str, value: str) -> None:\n    \"\"\"Manipulate the WinReg, and add value to the\n    environment variable if exists or create new.\n    \"\"\"\n    import winreg\n\n    value = os.path.normcase(value)\n\n    with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as root:\n        with winreg.OpenKey(root, \"Environment\", 0, winreg.KEY_ALL_ACCESS) as env_key:\n            try:\n                old_value, type_ = winreg.QueryValueEx(env_key, env_name)\n                paths = [os.path.normcase(item) for item in old_value.split(os.pathsep)]\n                if value in paths:\n                    return\n            except FileNotFoundError:\n                paths, type_ = [], winreg.REG_EXPAND_SZ\n            new_value = os.pathsep.join([value, *paths])\n            winreg.SetValueEx(env_key, env_name, 0, type_, new_value)\n\n\ndef format_resolution_impossible(err: ResolutionImpossible) -> str:\n    from pdm.resolver.python import PythonRequirement\n\n    causes: list[RequirementInformation] = err.causes\n    info_lines: set[str] = set()\n    if not causes:\n        return \"\"\n    if all(isinstance(cause.requirement, PythonRequirement) for cause in causes):\n        project_requires: PythonRequirement = next(cause.requirement for cause in causes if cause.parent is None)\n        pyspec = cast(PySpecSet, project_requires.specifier)\n        conflicting = [\n            cause\n            for cause in causes\n            if cause.parent is not None and not cause.requirement.specifier.is_superset(pyspec)\n        ]\n        result = [\n            \"Unable to find a resolution because the following dependencies don't work \"\n            \"on all Python versions in the range of the project's `requires-python`: \"\n            f\"[success]{pyspec}[/].\"\n        ]\n        for req, parent in conflicting:\n            pyspec &= req.specifier\n            info_lines.add(f\"  {req.as_line()} (from {parent!r})\")\n        result.extend(sorted(info_lines))\n        if pyspec.is_empty():\n            result.append(\"Consider changing the version specifiers of the dependencies to be compatible\")\n        else:\n            result.append(\n                \"A possible solution is to change the value of `requires-python` \"\n                f\"in pyproject.toml to [success]{pyspec}[/].\"\n            )\n        return \"\\n\".join(result)\n\n    if len(causes) == 1:\n        return (\n            \"Unable to find a resolution for \"\n            f\"[success]{causes[0].requirement.identify()}[/]\\n\"\n            \"Please make sure the package name is correct.\"\n        )\n\n    result = [\n        \"Unable to find a resolution for \"\n        f\"[success]{causes[0].requirement.identify()}[/]\\n\"\n        \"because of the following conflicts:\"\n    ]\n    for req, parent in causes:\n        info_lines.add(f\"  {req.as_line()} (from {parent if parent else 'project'})\")\n    result.extend(sorted(info_lines))\n    result.append(\n        \"To fix this, you could loosen the dependency version constraints in \"\n        \"pyproject.toml. See https://pdm-project.org/en/latest/usage/lockfile/\"\n        \"#solve-the-locking-failure for more details.\"\n    )\n    return \"\\n\".join(result)\n\n\ndef merge_dictionary(target: MutableMapping[Any, Any], input: Mapping[Any, Any], append_array: bool = True) -> None:\n    \"\"\"Merge the input dict with the target while preserving the existing values\n    properly. This will update the target dictionary in place.\n    List values will be extended, but only if the value is not already in the list.\n    \"\"\"\n    for key, value in input.items():\n        if key not in target:\n            target[key] = value\n        elif isinstance(target[key], dict):\n            merge_dictionary(target[key], value, append_array=append_array)\n        elif isinstance(target[key], list) and append_array:\n            target[key].extend(x for x in value if x not in target[key])\n            if hasattr(target[key], \"multiline\"):\n                target[key].multiline(True)  # type: ignore[attr-defined]\n        else:\n            target[key] = value\n\n\ndef is_pipx_installation() -> bool:\n    return sys.prefix.split(os.sep)[-3:-1] == [\"pipx\", \"venvs\"]\n\n\ndef is_homebrew_installation() -> bool:\n    return \"/libexec\" in sys.prefix.replace(\"\\\\\", \"/\")\n\n\ndef is_scoop_installation() -> bool:\n    return os.name == \"nt\" and is_path_relative_to(sys.prefix, Path.home() / \"scoop/apps/pdm\")\n\n\ndef get_dist_location(dist: Distribution) -> str:\n    direct_url = dist.read_text(\"direct_url.json\")\n    if not direct_url:\n        return \"\"\n    direct_url_data = json.loads(direct_url)\n    url = cast(str, direct_url_data[\"url\"])\n    if url.startswith(\"file:\"):\n        path = url_to_path(url)\n        editable = direct_url_data.get(\"dir_info\", {}).get(\"editable\", False)\n        return f\"{'-e ' if editable else ''}{path}\"\n    return \"\"\n\n\ndef get_pep582_path(project: Project) -> str:\n    from pdm.compat import resources_open_binary\n\n    script_dir = Path(__file__).parent.parent / \"pep582\"\n    if script_dir.exists():\n        return str(script_dir)\n\n    script_dir = project.global_config.config_file.parent / \"pep582\"\n    if script_dir.joinpath(\"sitecustomize.py\").exists():\n        return str(script_dir)\n    script_dir.mkdir(parents=True, exist_ok=True)\n    with resources_open_binary(\"pdm.pep582\", \"sitecustomize.py\") as f:\n        script_dir.joinpath(\"sitecustomize.py\").write_bytes(f.read())\n    return str(script_dir)\n\n\ndef use_venv(project: Project, name: str) -> None:\n    from pdm.cli.commands.venv.utils import get_venv_with_name\n    from pdm.environments import PythonEnvironment\n\n    venv = get_venv_with_name(project, cast(str, name))\n    project.core.ui.info(f\"In virtual environment: [success]{venv.root}[/]\", verbosity=termui.Verbosity.DETAIL)\n    project.environment = PythonEnvironment(project, python=str(venv.interpreter))\n\n\ndef normalize_pattern(pattern: str) -> str:\n    \"\"\"Normalize a pattern to a valid name for a package.\"\"\"\n    return re.sub(r\"[^A-Za-z0-9*?]+\", \"-\", pattern).lower()\n"
  },
  {
    "path": "src/pdm/compat.py",
    "content": "from __future__ import annotations\n\nimport importlib.resources\nimport sys\nfrom collections.abc import Iterator\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, ContextManager, Sequence, TypeVar\n\nif TYPE_CHECKING:\n    from typing import IO, Protocol\n\n    class SupportsIdentify(Protocol):\n        def identify(self) -> str: ...\n\n\nif sys.version_info >= (3, 11):\n    import tomllib\nelse:\n    import tomli as tomllib\n\nT = TypeVar(\"T\", bound=\"SupportsIdentify\")\n\nif (\n    not (sys.version_info[:2] == (3, 9) and sys.platform == \"win32\")\n    # a bug on windows+py39 that zipfile path is not normalized\n):\n\n    def resources_open_binary(package: str, resource: str) -> IO[bytes]:\n        return (importlib.resources.files(package) / resource).open(\"rb\")\n\n    def resources_read_text(package: str, resource: str, encoding: str = \"utf-8\", errors: str = \"strict\") -> str:\n        with (importlib.resources.files(package) / resource).open(\"r\", encoding=encoding, errors=errors) as f:\n            return f.read()\n\n    def resources_path(package: str, resource: str) -> ContextManager[Path]:\n        return importlib.resources.as_file(importlib.resources.files(package) / resource)\n\nelse:\n    resources_open_binary = importlib.resources.open_binary\n    resources_read_text = importlib.resources.read_text\n    resources_path = importlib.resources.path\n\n\nif sys.version_info >= (3, 10):\n    import importlib.metadata as importlib_metadata\nelse:\n    import importlib_metadata\n\n\nDistribution = importlib_metadata.Distribution\n\n\nclass CompatibleSequence(Sequence[T]):  # pragma: no cover\n    \"\"\"A compatibility class for Sequence that also exposes `items()`, `keys()` and `values()` methods\"\"\"\n\n    def __init__(self, data: Sequence[T]) -> None:\n        self._data = data\n\n    def __getitem__(self, index: str | slice | int) -> T | Sequence[T]:  # type: ignore[override]\n        if isinstance(index, str):\n            from pdm.utils import deprecation_warning\n\n            deprecation_warning(\n                \"__getitem__ with a string key is deprecated on the requirements collection. It's not a mapping but a list\",\n                stacklevel=2,\n            )\n            for r in self._data:\n                if r.identify() == index:\n                    return r\n            raise KeyError(index)\n        return self._data[index]\n\n    def __len__(self) -> int:\n        return len(self._data)\n\n    def __iter__(self) -> Iterator[T]:\n        return iter(self._data)\n\n    def keys(self) -> Sequence[str]:\n        from pdm.utils import deprecation_warning\n\n        deprecation_warning(\n            \".keys() is deprecated on the requirements collection, it's not a mapping but a list.\", stacklevel=2\n        )\n        return [r.identify() for r in self._data]\n\n    def values(self) -> Sequence[T]:\n        from pdm.utils import deprecation_warning\n\n        deprecation_warning(\n            \".values() is deprecated on the requirements collection, it's not a mapping but a list.\", stacklevel=2\n        )\n        return self._data\n\n    def items(self) -> Iterator[tuple[str, T]]:\n        from pdm.utils import deprecation_warning\n\n        deprecation_warning(\n            \".items() is deprecated on the requirements collection, it's not a mapping anymore.\", stacklevel=2\n        )\n        for r in self._data:\n            yield r.identify(), r\n\n\n__all__ = [\"CompatibleSequence\", \"Distribution\", \"importlib_metadata\", \"tomllib\"]\n"
  },
  {
    "path": "src/pdm/core.py",
    "content": "r\"\"\"\n    ____  ____  __  ___\n   / __ \\/ __ \\/  |/  /\n  / /_/ / / / / /|_/ /\n / ____/ /_/ / /  / /\n/_/   /_____/_/  /_/\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport contextlib\nimport dataclasses as dc\nimport importlib\nimport itertools\nimport os\nimport pkgutil\nimport sys\nfrom datetime import datetime\nfrom functools import cached_property\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom typing import TYPE_CHECKING, cast\n\nimport tomlkit.exceptions\n\nfrom pdm import termui\nfrom pdm.__version__ import __version__\nfrom pdm.cli.options import ignore_python_option, no_cache_option, non_interactive_option, pep582_option, verbose_option\nfrom pdm.cli.utils import ArgumentParser, ErrorArgumentParser, format_similar_command\nfrom pdm.compat import importlib_metadata\nfrom pdm.exceptions import PdmArgumentError, PdmUsageError\nfrom pdm.installers import InstallManager\nfrom pdm.models.repositories import BaseRepository, PyPIRepository\nfrom pdm.project import Project\nfrom pdm.project.config import Config\nfrom pdm.utils import is_in_zipapp\n\nif TYPE_CHECKING:\n    from typing import Any, Iterable\n\n    from pdm.cli.commands.base import BaseCommand\n    from pdm.project.config import ConfigItem\n\nCOMMANDS_MODULE_PATH = importlib.import_module(\"pdm.cli.commands\").__path__\n\n\n@dc.dataclass\nclass State:\n    \"\"\"State of the core object.\"\"\"\n\n    config_settings: dict[str, Any] | None = None\n    \"\"\"The config settings map shared by all packages\"\"\"\n    exclude_newer: datetime | None = None\n    \"\"\"The exclude newer than datetime for the lockfile\"\"\"\n    build_isolation: bool = True\n    \"\"\"Whether to make an isolated environment and install requirements for build\"\"\"\n    enable_cache: bool = True\n    \"\"\"Whether to enable the cache\"\"\"\n    overrides: list[str] = dc.field(default_factory=list)\n    \"\"\"The requirement overrides for the resolver\"\"\"\n\n\nclass Core:\n    \"\"\"A high level object that manages all classes and configurations\"\"\"\n\n    parser: argparse.ArgumentParser\n    subparsers: argparse._SubParsersAction\n\n    project_class = Project\n    repository_class: type[BaseRepository] = PyPIRepository\n    install_manager_class = InstallManager\n\n    def __init__(self) -> None:\n        self.version = __version__\n        self.exit_stack = contextlib.ExitStack()\n        self.ui = termui.UI(exit_stack=self.exit_stack)\n        self.state = State()\n        self.exit_stack.callback(setattr, self, \"config_settings\", None)\n        self.commands: list[str] = []\n        self.init_parser()\n        self.load_plugins()\n\n    def create_temp_dir(self, *args: Any, **kwargs: Any) -> str:\n        return self.exit_stack.enter_context(TemporaryDirectory(*args, **kwargs))\n\n    def init_parser(self) -> None:\n        self.parser = ErrorArgumentParser(\n            prog=\"pdm\",\n            description=__doc__,\n        )\n        self.parser.add_argument(\n            \"-V\",\n            \"--version\",\n            action=\"version\",\n            version=\"{}, version {}\".format(\n                termui.style(\"PDM\", style=\"bold\"),\n                termui.style(self.version, style=\"success\"),\n            ),\n            help=\"Show the version and exit\",\n        )\n        self.parser.add_argument(\n            \"-c\",\n            \"--config\",\n            help=\"Specify another config file path [env var: PDM_CONFIG_FILE] \",\n        )\n        verbose_option.add_to_parser(self.parser)\n        no_cache_option.add_to_parser(self.parser)\n        ignore_python_option.add_to_parser(self.parser)\n        pep582_option.add_to_parser(self.parser)\n        non_interactive_option.add_to_parser(self.parser)\n\n        self.subparsers = self.parser.add_subparsers(parser_class=ArgumentParser, title=\"commands\", metavar=\"\")\n        for _, name, _ in pkgutil.iter_modules(COMMANDS_MODULE_PATH):\n            module = importlib.import_module(f\"pdm.cli.commands.{name}\", __name__)\n            try:\n                klass = module.Command\n            except AttributeError:\n                continue\n            self.register_command(klass, klass.name or name)\n\n    def __call__(self, *args: Any, **kwargs: Any) -> None:\n        return self.main(*args, **kwargs)\n\n    def ensure_project(self, options: argparse.Namespace, obj: Project | None) -> Project:\n        if obj is not None:\n            project = obj\n        else:\n            global_project = bool(getattr(options, \"global_project\", None))\n\n            default_root = None if global_project or getattr(options, \"search_parent\", True) else \".\"\n            project = self.create_project(\n                getattr(options, \"project_path\", None) or default_root,\n                is_global=global_project,\n                global_config=options.config or os.getenv(\"PDM_CONFIG_FILE\"),\n            )\n        self.state.build_isolation = project.config[\"build_isolation\"]\n        return project\n\n    def create_project(\n        self,\n        root_path: str | Path | None = None,\n        is_global: bool = False,\n        global_config: str | None = None,\n    ) -> Project:\n        \"\"\"Create a new project object\n\n        Args:\n            root_path (PathLike): The path to the project root directory\n            is_global (bool): Whether the project is a global project\n            global_config (str): The path to the global config file\n\n        Returns:\n            The project object\n        \"\"\"\n        return self.project_class(self, root_path, is_global, global_config)\n\n    def handle(self, project: Project, options: argparse.Namespace) -> None:\n        \"\"\"Called before command invocation\"\"\"\n        from pdm.cli.commands.fix import Command as FixCommand\n        from pdm.cli.hooks import HookManager\n        from pdm.cli.utils import use_venv\n\n        self.ui.set_verbosity(options.verbose)\n        self.ui.set_theme(project.global_config.load_theme())\n        self.ui.log_dir = os.path.expanduser(cast(str, project.config[\"log_dir\"]))\n\n        command = cast(\"BaseCommand | None\", getattr(options, \"command\", None))\n        self.state.config_settings = getattr(options, \"config_setting\", None)\n\n        if options.no_cache:\n            self.state.enable_cache = False\n\n        hooks = HookManager(project, getattr(options, \"skip\", None))\n        hooks.try_emit(\"pre_invoke\", command=command.name if command else None, options=options)\n\n        if not isinstance(command, FixCommand):\n            FixCommand.check_problems(project)\n\n        for callback in getattr(options, \"callbacks\", []):\n            callback(project, options)\n\n        if lockfile := getattr(options, \"lockfile\", None):\n            project.set_lockfile(cast(str, lockfile))\n\n        if getattr(options, \"use_venv\", None):\n            use_venv(project, cast(str, options.use_venv))\n\n        if overrides := getattr(options, \"override\", None):\n            self.state.overrides = overrides\n\n        if command is None:\n            self.parser.print_help()\n            sys.exit(0)\n        command.handle(project, options)\n\n    @staticmethod\n    def get_command(args: list[str]) -> tuple[int, str]:\n        \"\"\"Get the command name from the arguments\"\"\"\n        options_with_values = (\"-c\", \"--config\")\n        need_value = False\n        for i, arg in enumerate(args):\n            if arg.startswith(\"-\"):\n                if not arg.startswith(options_with_values):\n                    continue\n                if (arg.startswith(\"-c\") and arg != \"-c\") or arg.startswith(\"--config=\"):\n                    continue\n                need_value = True\n            elif need_value:\n                need_value = False\n                continue\n            else:\n                return i, arg\n        return -1, \"\"\n\n    def _get_cli_args(self, args: list[str], obj: Project | None) -> list[str]:\n        project = self.create_project(is_global=False) if obj is None else obj\n        if project.is_global:\n            return args\n        try:\n            config = project.pyproject.settings.get(\"options\", {})\n        except tomlkit.exceptions.TOMLKitError as e:  # pragma: no cover\n            self.ui.error(f\"Failed to parse pyproject.toml: {e}\")\n            config = {}\n        (pos, command) = self.get_command(args)\n        if command and command in config:\n            # add args after the command\n            args[pos + 1 : pos + 1] = list(config[command])\n        return args\n\n    def main(\n        self,\n        args: list[str] | None = None,\n        prog_name: str | None = None,\n        obj: Project | None = None,\n        **extra: Any,\n    ) -> None:\n        \"\"\"The main entry function\"\"\"\n        if args is None:\n            args = []\n        args = self._get_cli_args(args, obj)\n        # Keep it for after project parsing to check if its a defined script\n        root_script = None\n        try:\n            options = self.parser.parse_args(args)\n        except PdmArgumentError as e:\n            # Failed to parse, try to give all to `run` command as shortcut\n            # and keep to root script (first non-dashed param) to check existence\n            # as soon as the project is parsed\n            _, root_script = self.get_command(args)\n            if not root_script:\n                self.parser.error(str(e.__cause__))\n            try:\n                options = self.parser.parse_args([\"run\", *args])\n            except PdmArgumentError as e:\n                self.parser.error(str(e.__cause__))\n\n        project = self.ensure_project(options, obj)\n        if root_script and root_script not in project.scripts:\n            message = format_similar_command(root_script, self.commands, list(project.scripts.keys()))\n            message = termui.style(message)\n            self.parser.error(message)\n\n        try:\n            self.handle(project, options)\n        except Exception:\n            etype, err, traceback = sys.exc_info()\n            should_show_tb = not isinstance(err, PdmUsageError) or self.ui.verbosity > termui.Verbosity.DETAIL\n            if self.ui.verbosity > termui.Verbosity.NORMAL and should_show_tb:\n                raise cast(Exception, err).with_traceback(traceback) from None\n            self.ui.echo(\n                rf\"[error]\\[{etype.__name__}][/]: {err}\",  # type: ignore[union-attr]\n                err=True,\n            )\n            if should_show_tb:\n                self.ui.warn(\"Add '-v' to see the detailed traceback\", verbosity=termui.Verbosity.NORMAL)\n            sys.exit(1)\n        else:\n            if project.config[\"check_update\"] and not is_in_zipapp():\n                from pdm.cli.actions import check_update\n\n                check_update(project)\n\n    def register_command(self, command: type[BaseCommand], name: str | None = None) -> None:\n        \"\"\"Register a subcommand to the subparsers,\n        with an optional name of the subcommand.\n\n        Args:\n            command (Type[pdm.cli.commands.base.BaseCommand]):\n                The command class to register\n            name (str): The name of the subcommand, if not given, `command.name`\n                is used\n        \"\"\"\n        assert self.subparsers\n        self.commands.append(name or command.name or \"\")\n        command.register_to(self.subparsers, name)\n\n    @staticmethod\n    def add_config(name: str, config_item: ConfigItem) -> None:\n        \"\"\"Add a config item to the configuration class.\n\n        Args:\n            name (str): The name of the config item\n            config_item (pdm.project.config.ConfigItem): The config item to add\n        \"\"\"\n        Config.add_config(name, config_item)\n\n    def _add_project_plugins_library(self) -> None:\n        project = self.create_project(is_global=False)\n        if project.is_global or not project.root.joinpath(\".pdm-plugins\").exists():\n            return\n\n        import site\n        import sysconfig\n\n        base = str(project.root / \".pdm-plugins\")\n        replace_vars = {\"base\": base, \"platbase\": base}\n\n        scheme_names = sysconfig.get_scheme_names()\n        if (sys.platform == \"darwin\" and \"osx_framework_library\" in scheme_names) or sys.platform == \"linux\":\n            scheme = \"posix_prefix\"\n        # sysconfig._get_default_scheme is a private function in 3.8 & 3.9\n        elif sys.version_info < (3, 10):\n            scheme = \"nt\" if os.name == \"nt\" else \"posix_prefix\"\n        else:\n            scheme = sysconfig.get_default_scheme()\n        purelib = sysconfig.get_path(\"purelib\", scheme, replace_vars)\n        scripts = sysconfig.get_path(\"scripts\", scheme, replace_vars)\n        site.addsitedir(purelib)\n        if os.path.exists(scripts):\n            os.environ[\"PATH\"] = os.pathsep.join([scripts, os.getenv(\"PATH\", \"\")])\n\n    def load_plugins(self) -> None:\n        \"\"\"Import and load plugins under `pdm.plugin` namespace\n        A plugin is a callable that accepts the core object as the only argument.\n\n        Example:\n            ```python\n            def my_plugin(core: pdm.core.Core) -> None:\n                ...\n            ```\n        \"\"\"\n        self._add_project_plugins_library()\n        entry_points: Iterable[importlib_metadata.EntryPoint] = itertools.chain(\n            importlib_metadata.entry_points(group=\"pdm\"),\n            importlib_metadata.entry_points(group=\"pdm.plugin\"),\n        )\n        for plugin in entry_points:\n            try:\n                plugin.load()(self)\n            except Exception as e:\n                self.ui.error(\n                    f\"Failed to load plugin {plugin.name}={plugin.value}: {e}\",\n                )\n\n    @cached_property\n    def uv_cmd(self) -> list[str]:\n        from pdm.compat import importlib_metadata\n\n        self.ui.info(\"Using uv is experimental and might break due to uv updates.\")\n        # First, try to find uv in Python modules\n        try:\n            importlib_metadata.distribution(\"uv\")\n        except ModuleNotFoundError:\n            pass\n        else:\n            return [sys.executable, \"-m\", \"uv\"]\n        # Try to find it in the typical place:\n        for bin_dir in [\".local/bin\", \".cargo/bin\"]:\n            if (uv_path := Path.home() / bin_dir / \"uv\").exists():\n                return [str(uv_path)]\n        # If not found, try to find it in PATH\n        import shutil\n\n        path = shutil.which(\"uv\")\n        if path:\n            return [path]\n        # If not found, try to find in the bin dir:\n        if (uv_path := Path(sys.argv[0]).with_name(\"uv\")).exists():\n            return [str(uv_path)]\n        raise PdmUsageError(\n            \"use_uv is enabled but can't find uv, please install it first: \"\n            \"https://docs.astral.sh/uv/getting-started/installation/\"\n        )\n\n\ndef main(args: list[str] | None = None) -> None:\n    \"\"\"The CLI entry function\"\"\"\n    core = Core()\n    with core.exit_stack:\n        return core.main(args or sys.argv[1:])\n"
  },
  {
    "path": "src/pdm/environments/__init__.py",
    "content": "from pdm.environments.base import BareEnvironment, BaseEnvironment\nfrom pdm.environments.local import PythonLocalEnvironment\nfrom pdm.environments.python import PythonEnvironment\n\n__all__ = [\n    \"BareEnvironment\",\n    \"BaseEnvironment\",\n    \"PythonEnvironment\",\n    \"PythonLocalEnvironment\",\n]\n"
  },
  {
    "path": "src/pdm/environments/base.py",
    "content": "from __future__ import annotations\n\nimport abc\nimport os\nimport re\nimport shutil\nimport subprocess\nimport sys\nimport tempfile\nimport weakref\nfrom contextlib import contextmanager\nfrom functools import cached_property\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Generator\n\nfrom pdm._types import NotSet, NotSetType\nfrom pdm.exceptions import BuildError, PdmUsageError\nfrom pdm.models.in_process import get_env_spec\nfrom pdm.models.markers import EnvSpec\nfrom pdm.models.python import PythonInfo\nfrom pdm.models.working_set import WorkingSet\nfrom pdm.utils import deprecation_warning, is_pip_compatible_with_python\n\nif TYPE_CHECKING:\n    import unearth\n    from httpx import BaseTransport\n\n    from pdm._types import RepositoryConfig\n    from pdm.models.auth import PdmBasicAuth\n    from pdm.models.session import PDMPyPIClient\n    from pdm.project import Project\n\n\nclass BaseEnvironment(abc.ABC):\n    \"\"\"Environment dependent stuff related to the selected Python interpreter.\"\"\"\n\n    project: Project\n    is_local = False\n\n    def __init__(self, project: Project, *, python: str | None = None) -> None:\n        \"\"\"\n        :param project: the project instance\n        \"\"\"\n\n        if isinstance(project, weakref.ProxyTypes):\n            self.project = project\n        else:\n            self.project = weakref.proxy(project)\n        self.python_requires = project.python_requires\n        if python is None:\n            self._interpreter = project.python\n        else:\n            self._interpreter = PythonInfo.from_path(python)\n\n    @cached_property\n    def auth(self) -> PdmBasicAuth:\n        from pdm.models.auth import PdmBasicAuth\n\n        return PdmBasicAuth(self.project.core.ui, self.project.sources)\n\n    @property\n    def is_global(self) -> bool:\n        \"\"\"For backward compatibility, it is opposite to ``is_local``.\"\"\"\n        return not self.is_local\n\n    @property\n    def interpreter(self) -> PythonInfo:\n        return self._interpreter\n\n    @abc.abstractmethod\n    def get_paths(self, dist_name: str | None = None) -> dict[str, str]:\n        \"\"\"Get paths like ``sysconfig.get_paths()`` for installation.\n\n        :param dist_name: The package name to be installed, if any.\n        \"\"\"\n        ...\n\n    @property\n    def process_env(self) -> dict[str, str]:\n        \"\"\"Get the process env var dict for the environment.\"\"\"\n        project = self.project\n        this_path = self.get_paths()[\"scripts\"]\n        python_root = os.path.dirname(project.python.executable)\n        new_path = os.pathsep.join([this_path, os.getenv(\"PATH\", \"\"), python_root])\n        return {\"PATH\": new_path, \"PDM_PROJECT_ROOT\": str(project.root)}\n\n    def _build_session(\n        self, sources: list[RepositoryConfig] | None = None, mounts: dict[str, BaseTransport | None] | None = None\n    ) -> PDMPyPIClient:\n        from pdm.models.session import PDMPyPIClient\n\n        if sources is None:\n            sources = self.project.sources\n\n        session = PDMPyPIClient(\n            sources=sources,\n            cache_dir=self.project.cache(\"http\") if self.project.core.state.enable_cache else None,\n            timeout=self.project.config[\"request_timeout\"],\n            auth=self.auth,\n            mounts=mounts,\n        )\n        self.project.core.exit_stack.callback(session.close)\n        return session\n\n    @cached_property\n    def session(self) -> PDMPyPIClient:\n        \"\"\"Build the session and cache it.\"\"\"\n        return self._build_session()\n\n    @contextmanager\n    def get_finder(\n        self,\n        sources: list[RepositoryConfig] | None = None,\n        ignore_compatibility: bool | NotSetType = NotSet,\n        minimal_version: bool = False,\n        env_spec: EnvSpec | None = None,\n    ) -> Generator[unearth.PackageFinder]:\n        \"\"\"Return the package finder of given index sources.\n\n        :param sources: a list of sources the finder should search in.\n        :param ignore_compatibility: (DEPRECATED)whether to ignore the python version\n            and wheel tags.\n        :param minimal_version: whether to find the minimal version of the package.\n        :param env_spec: the environment spec to filter the packages.\n        \"\"\"\n        from pdm.models.finder import PDMPackageFinder\n\n        if sources is None:\n            sources = self.project.sources\n        if not sources:\n            raise PdmUsageError(\n                \"You must specify at least one index in pyproject.toml or config.\\n\"\n                \"The 'pypi.ignore_stored_index' config value is \"\n                f\"{self.project.config['pypi.ignore_stored_index']}\"\n            )\n        if ignore_compatibility is not NotSet:  # pragma: no cover\n            deprecation_warning(\n                \"`ignore_compatibility` argument is deprecated, pass in `env_spec` instead.\\n\",\n                stacklevel=2,\n            )\n        else:\n            ignore_compatibility = False\n\n        if env_spec is None:\n            if ignore_compatibility:  # pragma: no cover\n                env_spec = self.allow_all_spec\n            else:\n                env_spec = self.spec\n\n        finder = PDMPackageFinder(\n            session=self.session,\n            env_spec=env_spec,\n            no_binary=self._setting_list(\"PDM_NO_BINARY\", \"resolution.no-binary\"),\n            only_binary=self._setting_list(\"PDM_ONLY_BINARY\", \"resolution.only-binary\"),\n            prefer_binary=self._setting_list(\"PDM_PREFER_BINARY\", \"resolution.prefer-binary\"),\n            respect_source_order=self.project.pyproject.settings.get(\"resolution\", {}).get(\n                \"respect-source-order\", False\n            ),\n            verbosity=self.project.core.ui.verbosity,\n            minimal_version=minimal_version,\n            exclude_newer_than=self.project.core.state.exclude_newer,\n        )\n        finder.sources.clear()\n        for source in sources:\n            assert source.url\n            if source.type == \"find_links\":\n                finder.add_find_links(source.url)\n            else:\n                finder.add_index_url(source.url)\n        yield finder\n\n    def _setting_list(self, var: str, key: str) -> list[str]:\n        \"\"\"\n        Get a list value, either comma separated or structured.\n\n        Returns `None` if both the environment variable and the key does not exists.\n        \"\"\"\n        if value := self.project.env_or_setting(var, key):\n            if isinstance(value, str):\n                value = [stripped for v in value.split(\",\") if (stripped := v.strip())]\n            return [stripped for v in value if (stripped := v.strip())]\n        return []\n\n    def get_working_set(self) -> WorkingSet:\n        \"\"\"Get the working set based on local packages directory.\"\"\"\n        paths = self.get_paths()\n        return WorkingSet([paths[\"platlib\"], paths[\"purelib\"]])\n\n    @cached_property\n    def spec(self) -> EnvSpec:\n        return get_env_spec(self.interpreter.executable.as_posix())\n\n    @property\n    def allow_all_spec(self) -> EnvSpec:\n        return EnvSpec(self.python_requires._logic)\n\n    def which(self, command: str) -> str | None:\n        \"\"\"Get the full path of the given executable against this environment.\"\"\"\n        if not os.path.isabs(command) and command.startswith(\"python\"):\n            match = re.match(r\"python(\\d(?:\\.\\d{1,2})?)\", command)\n            this_version = self.interpreter.version\n            if not match or str(this_version).startswith(match.group(1)):\n                return str(self.interpreter.executable)\n        # Fallback to use shutil.which to find the executable\n        this_path = self.get_paths()[\"scripts\"]\n        python_root = os.path.dirname(self.interpreter.executable)\n        new_path = os.pathsep.join([this_path, os.getenv(\"PATH\", \"\"), python_root])\n        return shutil.which(command, path=new_path)\n\n    def _download_pip_wheel(self, path: str | Path) -> None:  # pragma: no cover\n        from unearth import UnpackError\n\n        download_error = BuildError(\"Can't get a working copy of pip for the project\")\n        with self.get_finder([self.project.default_source]) as finder:\n            finder.only_binary = {\"pip\"}\n            best_match = finder.find_best_match(\"pip\").best\n            if not best_match:\n                raise download_error\n            with tempfile.TemporaryDirectory(prefix=\"pip-download-\") as dirname:\n                try:\n                    downloaded = finder.download_and_unpack(best_match.link, dirname, dirname)\n                except UnpackError as e:\n                    raise download_error from e\n                shutil.move(str(downloaded), path)\n\n    @cached_property\n    def pip_command(self) -> list[str]:\n        \"\"\"Get a pip command for this environment, and download one if not available.\n        Return a list of args like ['python', '-m', 'pip']\n        \"\"\"\n        try:\n            from pip import __file__ as pip_location\n        except ImportError:\n            pip_location = None  # type: ignore[assignment]\n\n        python_version = self.interpreter.version\n        executable = str(self.interpreter.executable)\n        proc = subprocess.run([executable, \"-Esm\", \"pip\", \"--version\"], capture_output=True)\n        if proc.returncode == 0:\n            # The pip has already been installed with the executable, just use it\n            command = [executable, \"-Esm\", \"pip\"]\n        elif pip_location and is_pip_compatible_with_python(python_version):\n            # Use the host pip package if available\n            command = [executable, \"-Es\", os.path.dirname(pip_location)]\n        else:\n            # Otherwise, download a pip wheel from the Internet.\n            pip_wheel = self.project.cache_dir / \"pip.whl\"\n            if not pip_wheel.is_file():\n                self._download_pip_wheel(pip_wheel)\n            command = [executable, str(pip_wheel / \"pip\")]\n        verbosity = self.project.core.ui.verbosity\n        if verbosity > 0:\n            command.append(\"-\" + \"v\" * verbosity)\n        return command\n\n    @property\n    def script_kind(self) -> str:\n        from dep_logic.tags.platform import Arch\n\n        if os.name != \"nt\":\n            return \"posix\"\n        if (arch := self.spec.platform.arch) == Arch.X86:  # pragma: no cover\n            return \"win-ia32\"\n        elif arch == Arch.Aarch64:  # pragma: no cover\n            return \"win-arm64\"\n        else:\n            return \"win-amd64\"\n\n\nclass BareEnvironment(BaseEnvironment):\n    \"\"\"Bare environment that does not depend on project files.\"\"\"\n\n    def __init__(self, project: Project) -> None:\n        super().__init__(project, python=sys.executable)\n\n    def get_paths(self, dist_name: str | None = None) -> dict[str, str]:\n        return {}\n\n    def get_working_set(self) -> WorkingSet:\n        if self.project.project_config.config_file.exists():\n            return self.project.get_environment().get_working_set()\n        else:\n            return WorkingSet([])\n"
  },
  {
    "path": "src/pdm/environments/local.py",
    "content": "from __future__ import annotations\n\nimport os\nimport re\nimport shlex\nfrom functools import cached_property\nfrom pathlib import Path\n\nfrom pdm.environments.base import BaseEnvironment\nfrom pdm.utils import pdm_scheme\n\n\ndef _get_shebang_path(executable: str, is_launcher: bool) -> bytes:\n    \"\"\"Get the interpreter path in the shebang line\n\n    The launcher can just use the command as-is.\n    Otherwise if the path contains whitespace or is too long, both distlib\n    and installer use a clever hack to make the shebang after ``/bin/sh``,\n    where the interpreter path is quoted.\n    \"\"\"\n    if is_launcher or (\" \" not in executable and (len(executable) + 3) <= 127):\n        return executable.encode(\"utf-8\")\n    return shlex.quote(executable).encode(\"utf-8\")\n\n\ndef _is_console_script(content: bytes) -> bool:\n    import io\n    import zipfile\n\n    if os.name == \"nt\":  # Windows .exe should be a zip file.\n        try:\n            with zipfile.ZipFile(io.BytesIO(content)) as zf:\n                return zf.namelist() == [\"__main__.py\"]\n        except zipfile.BadZipFile:\n            # Not a valid zip executable; fall back to text-based shebang detection below\n            pass\n\n    try:\n        text = content.decode(\"utf-8\")\n        return text.startswith(\"#!\")\n    except UnicodeDecodeError:\n        return False\n\n\ndef _replace_shebang(path: Path, new_executable: bytes) -> None:\n    \"\"\"Replace the python executable from the shebang line, which can be in two forms:\n\n    1. #!python_executable\n    2. #!/bin/sh\n       '''exec' '/path to/python' \"$0\" \"$@\"\n       ' '''\n    \"\"\"\n    _complex_shebang_re = rb\"^(#!/bin/sh\\n'''exec' )('.+?')( \\\"\\$0\\\")\"\n    _simple_shebang_re = rb\"^(#!)(.+?)\\s*(?=\\n)\"\n    contents = path.read_bytes()\n\n    if not _is_console_script(contents):\n        return\n\n    if os.name == \"nt\":\n        new_content, count = re.subn(_simple_shebang_re, rb\"\\1\" + new_executable, contents, count=1, flags=re.M)\n        if count > 0:\n            path.write_bytes(new_content)\n        return\n\n    new_content, count = re.subn(_complex_shebang_re, rb\"\\1\" + new_executable + rb\"\\3\", contents, count=1)\n    if count > 0:\n        path.write_bytes(new_content)\n        return\n\n    new_content, count = re.subn(_simple_shebang_re, rb\"\\1\" + new_executable, contents, count=1)\n    if count > 0:\n        path.write_bytes(new_content)\n\n\nclass PythonLocalEnvironment(BaseEnvironment):\n    \"\"\"A project environment that installs packages into\n    the local `__pypackages__` directory(PEP 582).\n    \"\"\"\n\n    is_local = True\n\n    @property\n    def process_env(self) -> dict[str, str]:\n        from pdm.cli.utils import get_pep582_path\n\n        env = super().process_env\n        pythonpath = os.getenv(\"PYTHONPATH\", \"\").split(os.pathsep)\n        pythonpath = [get_pep582_path(self.project)] + [p for p in pythonpath if \"/pep582\" not in p.replace(\"\\\\\", \"/\")]\n        env[\"PYTHONPATH\"] = os.pathsep.join(pythonpath)\n        env[\"PEP582_PACKAGES\"] = str(self.packages_path)\n        return env\n\n    @cached_property\n    def packages_path(self) -> Path:\n        \"\"\"The local packages path.\"\"\"\n        pypackages = self.project.root / \"__pypackages__\" / self.interpreter.identifier\n        if not pypackages.exists() and \"-32\" in pypackages.name:\n            compatible_packages = pypackages.with_name(pypackages.name[:-3])\n            if compatible_packages.exists():\n                pypackages = compatible_packages\n        scripts = \"Scripts\" if os.name == \"nt\" else \"bin\"\n        if not pypackages.parent.exists():\n            pypackages.parent.mkdir(parents=True)\n            pypackages.parent.joinpath(\".gitignore\").write_text(\"*\\n!.gitignore\\n\")\n        for subdir in [scripts, \"include\", \"lib\"]:\n            pypackages.joinpath(subdir).mkdir(exist_ok=True, parents=True)\n        return pypackages\n\n    def get_paths(self, dist_name: str | None = None) -> dict[str, str]:\n        scheme = pdm_scheme(self.packages_path.as_posix())\n        scheme[\"headers\"] = os.path.join(scheme[\"headers\"], dist_name or \"UNKNOWN\")\n        return scheme\n\n    def update_shebangs(self, new_path: str) -> None:\n        \"\"\"Update the shebang lines\"\"\"\n        scripts = self.get_paths()[\"scripts\"]\n        for child in Path(scripts).iterdir():\n            if not child.is_file() or child.suffix not in (\".exe\", \".py\", \"\"):\n                continue\n            is_launcher = child.suffix == \".exe\"\n            new_shebang = _get_shebang_path(new_path, is_launcher)\n            _replace_shebang(child, new_shebang)\n"
  },
  {
    "path": "src/pdm/environments/python.py",
    "content": "from __future__ import annotations\n\nimport os\nfrom typing import TYPE_CHECKING\n\nfrom pdm.environments.base import BaseEnvironment\nfrom pdm.models.in_process import get_sys_config_paths\nfrom pdm.models.working_set import WorkingSet\n\nif TYPE_CHECKING:\n    from pdm.project import Project\n\n\nclass PythonEnvironment(BaseEnvironment):\n    \"\"\"A project environment that is directly derived from a Python interpreter\"\"\"\n\n    def __init__(\n        self,\n        project: Project,\n        *,\n        python: str | None = None,\n        prefix: str | None = None,\n        extra_paths: list[str] | None = None,\n    ) -> None:\n        super().__init__(project, python=python)\n        self.prefix = prefix\n        self.extra_paths = extra_paths or []\n\n    def get_paths(self, dist_name: str | None = None) -> dict[str, str]:\n        is_venv = self.interpreter.get_venv() is not None\n        if self.prefix is not None:\n            replace_vars = {\"base\": self.prefix, \"platbase\": self.prefix}\n            kind = \"prefix\"\n        else:\n            replace_vars = None\n            kind = \"user\" if not is_venv and self.project.global_config[\"global_project.user_site\"] else \"default\"\n        paths = get_sys_config_paths(str(self.interpreter.executable), replace_vars, kind=kind)\n        if is_venv:\n            python_xy = f\"python{self.interpreter.identifier}\"\n            paths[\"include\"] = os.path.join(paths[\"data\"], \"include\", \"site\", python_xy)\n        elif not dist_name:\n            dist_name = \"UNKNOWN\"\n        if dist_name:\n            paths[\"include\"] = os.path.join(paths[\"include\"], dist_name)\n        paths[\"prefix\"] = paths[\"data\"]\n        paths[\"headers\"] = paths[\"include\"]\n        return paths\n\n    @property\n    def process_env(self) -> dict[str, str]:\n        env = super().process_env\n        venv = self.interpreter.get_venv()\n        if venv is not None and self.prefix is None:\n            env.update(venv.env_vars())\n        return env\n\n    def get_working_set(self) -> WorkingSet:\n        scheme = self.get_paths()\n        paths = [scheme[\"platlib\"], scheme[\"purelib\"]]\n        venv = self.interpreter.get_venv()\n        shared_paths = self.extra_paths[:]\n        if venv is not None and venv.include_system_site_packages:\n            shared_paths.extend(venv.base_paths)\n        return WorkingSet(paths, shared_paths=list(dict.fromkeys(shared_paths)))\n"
  },
  {
    "path": "src/pdm/exceptions.py",
    "content": "from __future__ import annotations\n\nimport warnings\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from pdm.models.candidates import Candidate\n\n\nclass PdmException(Exception):\n    pass\n\n\nclass ResolutionError(PdmException):\n    pass\n\n\nclass PdmArgumentError(PdmException):\n    pass\n\n\nclass PdmUsageError(PdmException):\n    pass\n\n\nclass RequirementError(PdmUsageError, ValueError):\n    pass\n\n\nclass PublishError(PdmUsageError):\n    pass\n\n\nclass InvalidPyVersion(PdmUsageError, ValueError):\n    pass\n\n\nclass CandidateNotFound(PdmException):\n    pass\n\n\nclass CandidateInfoNotFound(PdmException):\n    def __init__(self, candidate: Candidate) -> None:\n        message = f\"No metadata information is available for [success]{candidate!s}[/].\"\n        self.candidate = candidate\n        super().__init__(message)\n\n\nclass PDMWarning(Warning):\n    pass\n\n\nclass PackageWarning(PDMWarning):\n    pass\n\n\nclass PDMDeprecationWarning(PDMWarning, DeprecationWarning):\n    pass\n\n\nwarnings.simplefilter(\"default\", category=PDMDeprecationWarning)\n\n\nclass ExtrasWarning(PDMWarning):\n    def __init__(self, project_name: str, extras: list[str]) -> None:\n        super().__init__(f\"Extras not found for {project_name}: [{','.join(extras)}]\")\n        self.extras = tuple(extras)\n\n\nclass ProjectError(PdmUsageError):\n    pass\n\n\nclass InstallationError(PdmException):\n    pass\n\n\nclass UninstallError(PdmException):\n    pass\n\n\nclass NoConfigError(PdmUsageError, KeyError):\n    def __str__(self) -> str:\n        return f\"No such config key: {self.args[0]!r}\"\n\n\nclass NoPythonVersion(PdmUsageError):\n    pass\n\n\nclass BuildError(PdmException, RuntimeError):\n    pass\n"
  },
  {
    "path": "src/pdm/formats/__init__.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, cast\n\nfrom pdm.formats import flit, pipfile, poetry, requirements, setup_py\nfrom pdm.formats.base import MetaConvertError as MetaConvertError\n\nif TYPE_CHECKING:\n    from argparse import Namespace\n    from pathlib import Path\n    from typing import Iterable, Mapping, Protocol, Union\n\n    from pdm.models.candidates import Candidate\n    from pdm.models.requirements import Requirement\n    from pdm.project import Project\n\n    ExportItems = Union[Iterable[Candidate], Iterable[Requirement]]\n\n    class _Format(Protocol):\n        def check_fingerprint(self, project: Project | None, filename: str | Path) -> bool: ...\n\n        def convert(\n            self,\n            project: Project | None,\n            filename: str | Path,\n            options: Namespace | None,\n        ) -> tuple[Mapping, Mapping]: ...\n\n        def export(self, project: Project, candidates: ExportItems, options: Namespace | None) -> str: ...\n\n\nFORMATS: Mapping[str, _Format] = {\n    \"pipfile\": cast(\"_Format\", pipfile),\n    \"poetry\": cast(\"_Format\", poetry),\n    \"flit\": cast(\"_Format\", flit),\n    \"setuppy\": cast(\"_Format\", setup_py),\n    \"requirements\": cast(\"_Format\", requirements),\n}\n"
  },
  {
    "path": "src/pdm/formats/base.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any, Callable, Mapping, TypeVar, cast\n\nimport tomlkit\n\nfrom pdm import termui\n\n_T = TypeVar(\"_T\", bound=Callable)\n\n\ndef convert_from(field: str | None = None, name: str | None = None) -> Callable[[_T], _T]:\n    def wrapper(func: _T) -> _T:\n        func._convert_from = field  # type: ignore[attr-defined]\n        func._convert_to = name  # type: ignore[attr-defined]\n        return func\n\n    return wrapper\n\n\nclass Unset(Exception):\n    pass\n\n\nclass MetaConvertError(Exception):\n    \"\"\"A special exception that preserves the partial metadata that are already resolved.\"\"\"\n\n    def __init__(self, errors: list[str], *, data: dict[str, Any], settings: dict[str, Any]) -> None:\n        self.errors = errors\n        self.data = data\n        self.settings = settings\n\n    def __str__(self) -> str:\n        return \"\\n\" + \"\\n\".join(self.errors)\n\n\nclass _MetaConverterMeta(type):\n    def __init__(cls, name: str, bases: tuple[type, ...], ns: dict[str, Any]) -> None:\n        super().__init__(name, bases, ns)\n        cls._converters = {}\n        _default = object()\n        for key, value in ns.items():\n            if getattr(value, \"_convert_from\", _default) is not _default:\n                name = value._convert_to or key\n                cls._converters[name] = value\n\n\nclass MetaConverter(metaclass=_MetaConverterMeta):\n    \"\"\"Convert a metadata dictionary to PDM's format\"\"\"\n\n    _converters: dict[str, Callable]\n\n    def __init__(self, source: dict, ui: termui.UI | None = None) -> None:\n        self.source = source\n        self.settings: dict[str, Any] = {}\n        self._data: dict[str, Any] = {}\n        self._ui = ui\n\n    def convert(self) -> tuple[Mapping[str, Any], Mapping[str, Any]]:\n        source = self.source\n        errors: list[str] = []\n        for key, func in self._converters.items():\n            if func._convert_from and func._convert_from not in source:  # type: ignore[attr-defined]\n                continue\n            value = source if func._convert_from is None else source[func._convert_from]  # type: ignore[attr-defined]\n            try:\n                self._data[key] = func(self, value)\n            except Unset:\n                pass\n            except Exception as e:\n                errors.append(f\"{key}: {e}\")\n\n        # Delete all used fields\n        for func in self._converters.values():\n            if func._convert_from is None:  # type: ignore[attr-defined]\n                continue\n            try:\n                del source[func._convert_from]  # type: ignore[attr-defined]\n            except KeyError:\n                pass\n        # Add remaining items to the data\n        self._data.update(source)\n        if errors:\n            raise MetaConvertError(errors, data=self._data, settings=self.settings)\n        return self._data, self.settings\n\n\ndef make_inline_table(data: Mapping) -> dict:\n    \"\"\"Create an inline table from the given data.\"\"\"\n    table = cast(dict, tomlkit.inline_table())\n    table.update(data)\n    return table\n\n\ndef make_array(data: list, multiline: bool = False) -> list:\n    array = cast(list, tomlkit.array().multiline(multiline))\n    if not data:\n        return array\n    array.extend(data)\n    return array\n\n\ndef array_of_inline_tables(value: list[Mapping], multiline: bool = True) -> list[dict]:\n    return make_array([make_inline_table(item) for item in value], multiline)\n"
  },
  {
    "path": "src/pdm/formats/flit.py",
    "content": "from __future__ import annotations\n\nimport ast\nimport os\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, Mapping, cast\n\nfrom pdm.compat import tomllib\nfrom pdm.formats.base import (\n    MetaConverter,\n    Unset,\n    array_of_inline_tables,\n    convert_from,\n    make_array,\n    make_inline_table,\n)\nfrom pdm.utils import cd\n\nif TYPE_CHECKING:\n    from argparse import Namespace\n    from os import PathLike\n\n    from pdm.project import Project\n\n\ndef check_fingerprint(project: Project | None, filename: PathLike) -> bool:\n    with open(filename, \"rb\") as fp:\n        try:\n            data = tomllib.load(fp)\n        except tomllib.TOMLDecodeError:\n            return False\n\n    return \"tool\" in data and \"flit\" in data[\"tool\"]\n\n\ndef _get_author(metadata: dict[str, Any], type_: str = \"author\") -> list[str]:\n    name = metadata.pop(type_)\n    email = metadata.pop(f\"{type_}-email\", None)\n    return cast(list[str], array_of_inline_tables([{\"name\": name, \"email\": email}]))\n\n\ndef get_docstring_and_version_via_ast(\n    target: Path,\n) -> tuple[str | None, str | None]:\n    \"\"\"\n    This function is borrowed from flit's implementation, but does not attempt to import\n    that file. If docstring or version can't be retrieved by this function,\n    they are just left empty.\n    \"\"\"\n    # read as bytes to enable custom encodings\n    if not target.exists():\n        return None, None\n    node = ast.parse(target.read_bytes())\n    for child in node.body:\n        # Only use the version from the given module if it's a simple\n        # string assignment to __version__\n        is_version_str = (\n            isinstance(child, ast.Assign)\n            and len(child.targets) == 1\n            and isinstance(child.targets[0], ast.Name)\n            and child.targets[0].id == \"__version__\"\n            and isinstance(child.value, ast.Constant)\n            and isinstance(child.value.value, str)\n        )\n        if is_version_str:\n            version: str | None = child.value.value  # type: ignore[assignment, attr-defined]\n            break\n    else:\n        version = None\n    return ast.get_docstring(node), version\n\n\nclass FlitMetaConverter(MetaConverter):\n    def warn_against_dynamic_version_or_docstring(self, source: Path, version: str, description: str) -> None:\n        if not self._ui:\n            return\n        dynamic_fields = []\n        if not version:\n            dynamic_fields.append(\"version\")\n        if not description:\n            dynamic_fields.append(\"description\")\n        if not dynamic_fields:\n            return\n        fields = \" and \".join(dynamic_fields)\n        message = (\n            f\"Can't retrieve {fields} from pyproject.toml or parsing {source}. \"\n            \"They are probably imported from other files which is not supported by PDM.\"\n            \" You may need to supply their values in pyproject.toml manually.\"\n        )\n        self._ui.warn(message)\n\n    @convert_from(\"metadata\")\n    def name(self, metadata: dict[str, Any]) -> str:\n        # name\n        module = metadata.pop(\"module\")\n        self._data[\"name\"] = metadata.pop(\"dist-name\", module)\n        # version and description\n        if (Path(module) / \"__init__.py\").exists():\n            source = Path(module) / \"__init__.py\"\n        else:\n            source = Path(f\"{module}.py\")\n\n        version = self._data.get(\"version\")\n        description = self._data.get(\"description\")\n        description_in_ast, version_in_ast = get_docstring_and_version_via_ast(source)\n        self._data[\"version\"] = version or version_in_ast or \"\"\n        self._data[\"description\"] = description or description_in_ast or \"\"\n        self.warn_against_dynamic_version_or_docstring(source, self._data[\"version\"], self._data[\"description\"])\n        # author and maintainer\n        if \"author\" in metadata:\n            self._data[\"authors\"] = _get_author(metadata)\n        if \"maintainer\" in metadata:\n            self._data[\"maintainers\"] = _get_author(metadata, \"maintainer\")\n        if \"license\" in metadata:\n            self._data[\"license\"] = make_inline_table({\"text\": metadata.pop(\"license\")})\n            self._data[\"dynamic\"] = [\"classifiers\"]\n        if \"urls\" in metadata:\n            self._data[\"urls\"] = metadata.pop(\"urls\")\n        if \"home-page\" in metadata:\n            self._data.setdefault(\"urls\", {})[\"homepage\"] = metadata.pop(\"home-page\")\n        if \"description-file\" in metadata:\n            self._data[\"readme\"] = metadata.pop(\"description-file\")\n        if \"requires-python\" in metadata:\n            self._data[\"requires-python\"] = metadata.pop(\"requires-python\")\n            self._data[\"dynamic\"] = [\"classifiers\"]\n        # requirements\n        self._data[\"dependencies\"] = make_array(metadata.pop(\"requires\", []), True)\n        self._data[\"optional-dependencies\"] = metadata.pop(\"requires-extra\", {})\n        # Add remaining metadata as the same key\n        self._data.update(metadata)\n        return self._data[\"name\"]\n\n    @convert_from(\"entrypoints\", name=\"entry-points\")\n    def entry_points(self, value: dict[str, dict[str, str]]) -> dict[str, dict[str, str]]:\n        return value\n\n    @convert_from(\"sdist\")\n    def includes(self, value: dict[str, list[str]]) -> None:\n        self.settings.setdefault(\"build\", {}).update(\n            {\"excludes\": value.get(\"exclude\"), \"includes\": value.get(\"include\")}\n        )\n        raise Unset()\n\n\ndef convert(project: Project | None, filename: PathLike, options: Namespace | None) -> tuple[Mapping, Mapping]:\n    with open(filename, \"rb\") as fp, cd(os.path.dirname(os.path.abspath(filename))):\n        converter = FlitMetaConverter(tomllib.load(fp)[\"tool\"][\"flit\"], project.core.ui if project else None)\n        return converter.convert()\n\n\ndef export(project: Project, candidates: list, options: Namespace | None) -> None:\n    raise NotImplementedError()\n"
  },
  {
    "path": "src/pdm/formats/pipfile.py",
    "content": "from __future__ import annotations\n\nimport functools\nimport operator\nimport os\nfrom typing import TYPE_CHECKING, Any\n\nfrom packaging.markers import default_environment\n\nfrom pdm.compat import tomllib\nfrom pdm.formats.base import make_array\nfrom pdm.models.markers import Marker, get_marker\nfrom pdm.models.requirements import FileRequirement, Requirement\n\nif TYPE_CHECKING:\n    from argparse import Namespace\n    from os import PathLike\n\n    from pdm._types import RequirementDict\n    from pdm.models.backends import BuildBackend\n    from pdm.project import Project\n\nMARKER_KEYS = list(default_environment().keys())\n\n\ndef convert_pipfile_requirement(name: str, req: RequirementDict, backend: BuildBackend) -> str:\n    if isinstance(req, dict):\n        markers: list[Marker] = []\n        if \"markers\" in req:\n            markers.append(get_marker(req[\"markers\"]))  # type: ignore[arg-type]\n        for key in MARKER_KEYS:\n            if key in req:\n                marker = get_marker(f\"{key}{req[key]}\")\n                markers.append(marker)\n                del req[key]\n\n        if markers:\n            marker = functools.reduce(operator.and_, markers)\n            req[\"marker\"] = str(marker).replace('\"', \"'\")\n    r = Requirement.from_req_dict(name, req)\n    if isinstance(r, FileRequirement):\n        r.relocate(backend)\n    return r.as_line()\n\n\ndef check_fingerprint(project: Project, filename: PathLike) -> bool:\n    return os.path.basename(filename) == \"Pipfile\"\n\n\ndef convert(project: Project, filename: PathLike, options: Namespace | None) -> tuple[dict[str, Any], dict[str, Any]]:\n    with open(filename, \"rb\") as fp:\n        data = tomllib.load(fp)\n    result = {}\n    settings: dict[str, Any] = {}\n    backend = project.backend\n    if \"pipenv\" in data and \"allow_prereleases\" in data[\"pipenv\"]:\n        settings.setdefault(\"resolution\", {})[\"allow-prereleases\"] = data[\"pipenv\"][\"allow_prereleases\"]\n    if \"requires\" in data:\n        python_version = data[\"requires\"].get(\"python_full_version\") or data[\"requires\"].get(\"python_version\")\n        result[\"requires-python\"] = f\">={python_version}\"\n    if \"source\" in data:\n        settings[\"source\"] = data[\"source\"]\n    result[\"dependencies\"] = make_array(  # type: ignore[assignment]\n        [convert_pipfile_requirement(k, req, backend) for k, req in data.get(\"packages\", {}).items()],\n        True,\n    )\n    settings[\"dev-dependencies\"] = {\n        \"dev\": make_array(\n            [convert_pipfile_requirement(k, req, backend) for k, req in data.get(\"dev-packages\", {}).items()],\n            True,\n        )\n    }\n    return result, settings\n\n\ndef export(project: Project, candidates: list, options: Any) -> None:\n    raise NotImplementedError()\n"
  },
  {
    "path": "src/pdm/formats/poetry.py",
    "content": "from __future__ import annotations\n\nimport functools\nimport operator\nimport os\nimport re\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, Iterable, Mapping, cast\n\nfrom pdm.compat import tomllib\nfrom pdm.formats.base import (\n    MetaConverter,\n    Unset,\n    array_of_inline_tables,\n    convert_from,\n    make_array,\n    make_inline_table,\n)\nfrom pdm.models.markers import Marker, get_marker\nfrom pdm.models.requirements import FileRequirement, Requirement\nfrom pdm.models.specifiers import PySpecSet\nfrom pdm.utils import cd\n\nif TYPE_CHECKING:\n    from argparse import Namespace\n\n    from pdm._types import RequirementDict\n    from pdm.project import Project\n\n\ndef check_fingerprint(project: Project | None, filename: Path | str) -> bool:\n    if Path(filename).name != \"pyproject.toml\":\n        return False\n    with open(filename, \"rb\") as fp:\n        try:\n            data = tomllib.load(fp)\n        except tomllib.TOMLDecodeError:\n            return False\n\n    return \"tool\" in data and \"poetry\" in data[\"tool\"]\n\n\nVERSION_RE = re.compile(r\"([^\\d\\s]*)\\s*(\\d.*?)\\s*(?=,|$)\")\n\n\ndef _convert_specifier(version: str) -> str:\n    parts = []\n    for op, ver in VERSION_RE.findall(str(version)):\n        if op == \"~\":\n            op += \"=\"\n        elif op == \"^\":\n            major, *vparts = ver.split(\".\")\n            next_major = \".\".join([str(int(major) + 1)] + [\"0\"] * len(vparts))\n            parts.append(f\">={ver},<{next_major}\")\n            continue\n        elif not op:\n            op = \"==\"\n        parts.append(f\"{op}{ver}\")\n    return \",\".join(parts)\n\n\ndef _convert_python(python: str) -> PySpecSet:\n    if not python:\n        return PySpecSet()\n    parts = [PySpecSet(_convert_specifier(s)) for s in python.split(\"||\")]\n    return functools.reduce(operator.or_, parts)\n\n\ndef _convert_req(name: str, req_dict: RequirementDict | list[RequirementDict]) -> Iterable[str]:\n    from pdm.models.backends import DEFAULT_BACKEND\n\n    backend = DEFAULT_BACKEND(Path.cwd())\n\n    def fix_req_path(req: Requirement) -> Requirement:\n        if isinstance(req, FileRequirement):\n            req.relocate(backend)\n        return req\n\n    if isinstance(req_dict, list):\n        for req in req_dict:\n            yield from _convert_req(name, req)\n    elif isinstance(req_dict, str):\n        pdm_req = fix_req_path(Requirement.from_req_dict(name, _convert_specifier(req_dict)))\n        yield pdm_req.as_line()\n    else:\n        assert isinstance(req_dict, dict)\n        req_dict = dict(req_dict)\n        req_dict.pop(\"optional\", None)  # Ignore the 'optional' key\n        if \"version\" in req_dict:\n            req_dict[\"version\"] = _convert_specifier(str(req_dict[\"version\"]))\n        markers: list[Marker] = []\n        if \"markers\" in req_dict:\n            markers.append(get_marker(req_dict.pop(\"markers\")))  # type: ignore[arg-type]\n        if \"python\" in req_dict:\n            markers.append(get_marker(_convert_python(str(req_dict.pop(\"python\"))).as_marker_string()))\n        if markers:\n            req_dict[\"marker\"] = str(functools.reduce(operator.and_, markers)).replace('\"', \"'\")\n        if \"rev\" in req_dict or \"branch\" in req_dict or \"tag\" in req_dict:\n            req_dict[\"ref\"] = req_dict.pop(\n                \"rev\",\n                req_dict.pop(\"tag\", req_dict.pop(\"branch\", None)),  # type: ignore[arg-type]\n            )\n        pdm_req = fix_req_path(Requirement.from_req_dict(name, req_dict))\n        yield pdm_req.as_line()\n\n\n# See https://github.com/python-poetry/poetry-core/pull/521#issuecomment-1327689551\n# for reasoning why email.utils.parseaddr is not used here.\nNAME_EMAIL_RE = re.compile(\n    r\"^(?P<name>[- .,\\w'’\\\"():&]+)(?: <(?P<email>.+?)>)?$\",  # noqa: RUF001\n    re.UNICODE,\n)\n\n\ndef parse_name_email(name_email: list[str]) -> list[dict]:\n    return array_of_inline_tables(\n        [\n            {\n                k: v\n                for k, v in NAME_EMAIL_RE.match(item).groupdict().items()  # type: ignore[union-attr]\n                if v is not None\n            }\n            for item in name_email\n        ]\n    )\n\n\nclass PoetryMetaConverter(MetaConverter):\n    @convert_from(\"authors\")\n    def authors(self, value: list[str]) -> list[dict]:\n        return parse_name_email(value)\n\n    @convert_from(\"maintainers\")\n    def maintainers(self, value: list[str]) -> list[dict]:\n        return parse_name_email(value)\n\n    @convert_from(\"license\")\n    def license(self, value: str) -> dict[str, str]:\n        return make_inline_table({\"text\": value})\n\n    @convert_from(name=\"requires-python\")\n    def requires_python(self, source: dict[str, Any]) -> str:\n        python = source.get(\"dependencies\", {}).pop(\"python\", None)\n        return str(_convert_python(python))\n\n    @convert_from()\n    def urls(self, source: dict[str, Any]) -> dict[str, str]:\n        rv = source.pop(\"urls\", {})\n        if \"homepage\" in source:\n            rv[\"homepage\"] = source.pop(\"homepage\")\n        if \"repository\" in source:\n            rv[\"repository\"] = source.pop(\"repository\")\n        if \"documentation\" in source:\n            rv[\"documentation\"] = source.pop(\"documentation\")\n        return rv\n\n    @convert_from(\"plugins\", name=\"entry-points\")\n    def entry_points(self, value: dict[str, dict[str, str]]) -> dict[str, dict[str, str]]:\n        return value\n\n    @convert_from()\n    def dependencies(self, source: dict[str, Any]) -> list[str]:\n        rv = []\n        value, extras = dict(source[\"dependencies\"]), source.pop(\"extras\", {})\n        for key, req_dict in value.items():\n            optional = getattr(req_dict, \"items\", None) and req_dict.pop(\"optional\", False)\n            for req in _convert_req(key, req_dict):\n                if optional:\n                    extra = next((k for k, v in extras.items() if key in v), None)\n                    if extra:\n                        self._data.setdefault(\"optional-dependencies\", {}).setdefault(extra, []).append(req)\n                else:\n                    rv.append(req)\n        del source[\"dependencies\"]\n        return make_array(rv, True)\n\n    @convert_from(\"dev-dependencies\")\n    def dev_dependencies(self, value: dict) -> None:\n        self.settings.setdefault(\"dev-dependencies\", {})[\"dev\"] = make_array(\n            [r for key, req in value.items() for r in _convert_req(key, req)], True\n        )\n        raise Unset()\n\n    @convert_from(\"group\")\n    def group_dependencies(self, value: dict[str, dict[str, Any]]) -> None:\n        for name, group in value.items():\n            self.settings.setdefault(\"dev-dependencies\", {})[name] = make_array(\n                [r for key, req in group.get(\"dependencies\", {}).items() for r in _convert_req(key, req)], True\n            )\n        raise Unset()\n\n    @convert_from(\"package-mode\")\n    def package_mode(self, value: bool) -> None:\n        self.settings[\"distribution\"] = value\n        raise Unset()\n\n    @convert_from()\n    def includes(self, source: dict[str, list[str] | str]) -> list[str]:\n        includes: list[str] = []\n        source_includes: list[str] = []\n        for item in source.pop(\"packages\", []):\n            assert isinstance(item, dict)\n            include = item[\"include\"]\n            if item.get(\"from\"):\n                include = Path(str(item.get(\"from\")), include).as_posix()\n            includes.append(include)\n        for item in source.pop(\"include\", []):\n            if not isinstance(item, dict):\n                includes.append(item)\n            else:\n                dest = source_includes if \"sdist\" in item.get(\"format\", \"\") else includes\n                dest.append(item[\"path\"])\n        if includes:\n            self.settings.setdefault(\"build\", {})[\"includes\"] = includes\n        raise Unset()\n\n    @convert_from(\"exclude\")\n    def excludes(self, value: list[str]) -> None:\n        self.settings.setdefault(\"build\", {})[\"excludes\"] = value\n        raise Unset()\n\n    @convert_from(\"build\")\n    def build(self, value: str | dict) -> None:\n        result = {}\n        if isinstance(value, dict) and \"generate-setup-file\" in value:\n            result[\"run-setuptools\"] = cast(bool, value[\"generate-setup-file\"])\n        self.settings.setdefault(\"build\", {}).update(result)\n        raise Unset()\n\n    @convert_from(\"source\")\n    def sources(self, value: list[dict[str, Any]]) -> None:\n        self.settings[\"source\"] = [\n            {\n                \"name\": item.get(\"name\", \"\"),\n                \"url\": item.get(\"url\", \"\"),\n                \"verify_ssl\": item.get(\"url\", \"\").startswith(\"https\"),\n            }\n            for item in value\n        ]\n        raise Unset()\n\n\ndef convert(\n    project: Project | None,\n    filename: str | Path,\n    options: Namespace | None,\n) -> tuple[Mapping[str, Any], Mapping[str, Any]]:\n    with open(filename, \"rb\") as fp, cd(os.path.dirname(os.path.abspath(filename))):\n        converter = PoetryMetaConverter(tomllib.load(fp)[\"tool\"][\"poetry\"], project.core.ui if project else None)\n        return converter.convert()\n\n\ndef export(project: Project, candidates: list, options: Any) -> None:\n    raise NotImplementedError()\n"
  },
  {
    "path": "src/pdm/formats/pylock.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any, Iterable\n\nimport tomlkit\nfrom dep_logic.markers import AnyMarker, BaseMarker, MarkerUnion, parse_marker\n\nfrom pdm.exceptions import ProjectError\nfrom pdm.formats.base import make_array, make_inline_table\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.requirements import FileRequirement, VcsRequirement\nfrom pdm.project.lockfile import FLAG_INHERIT_METADATA\nfrom pdm.utils import cd, normalize_name\n\nif TYPE_CHECKING:\n    from pdm.models.markers import Marker\n    from pdm.models.repositories.lock import LockedRepository, Package\n    from pdm.project import Project\n\n\ndef _group_sort_key(group: str) -> tuple[bool, str]:\n    return group != \"default\", group\n\n\nclass PyLockConverter:\n    lock_version = \"1.0\"\n\n    def __init__(self, project: Project, locked_repository: LockedRepository) -> None:\n        self.project = project\n        self.locked_repository = locked_repository\n\n    def make_package(self, package: Package) -> dict[str, Any]:\n        req = package.candidate.req\n        candidate = package.candidate\n        result: dict[str, Any] = {\n            \"name\": candidate.req.key,\n            \"version\": candidate.version,\n        }\n        if candidate.requires_python:\n            result[\"requires-python\"] = candidate.requires_python\n        if isinstance(req, VcsRequirement):\n            result[\"vcs\"] = {\n                \"type\": req.vcs,\n                \"url\": req.repo,\n                \"commit-id\": candidate.get_revision(),\n            }\n            if req.ref:\n                result[\"vcs\"][\"requested-revision\"] = req.ref\n            if req.subdirectory:\n                result[\"vcs\"][\"subdirectory\"] = req.subdirectory\n        elif isinstance(req, FileRequirement):\n            if req.is_local_dir:\n                result[\"directory\"] = {\n                    \"path\": req.str_path,\n                    \"editable\": req.editable,\n                }\n                if req.subdirectory:\n                    result[\"directory\"][\"subdirectory\"] = req.subdirectory\n            else:\n                archive: dict[str, Any]\n                archive = result[\"archive\"] = {\"url\": req.get_full_url()}\n                if req.path:\n                    archive[\"path\"] = req.str_path\n                for hash_item in candidate.hashes:\n                    hash_type, hash_value = hash_item[\"hash\"].split(\":\", 1)\n                    archive.setdefault(\"hashes\", tomlkit.inline_table())[hash_type] = hash_value\n        else:\n            wheels: list[dict[str, Any]] = tomlkit.array().multiline(True)\n            for hash_item in candidate.hashes:\n                hash_type, hash_value = hash_item[\"hash\"].split(\":\", 1)\n                if hash_item.get(\"file\", \"\").endswith(\".whl\") or hash_item.get(\"url\", \"\").endswith(\".whl\"):\n                    wheel: dict[str, Any] = {}\n                    if \"file\" in hash_item:\n                        wheel[\"name\"] = hash_item[\"file\"]\n                    if \"url\" in hash_item:\n                        wheel[\"url\"] = hash_item[\"url\"]\n                    wheel[\"hashes\"] = make_inline_table({hash_type: hash_value})\n                    wheels.append(wheel)\n                else:\n                    sdist: dict[str, Any] = {}\n                    if \"file\" in hash_item:\n                        sdist[\"name\"] = hash_item[\"file\"]\n                    if \"url\" in hash_item:\n                        sdist[\"url\"] = hash_item[\"url\"]\n                    sdist[\"hashes\"] = make_inline_table({hash_type: hash_value})\n                    result[\"sdist\"] = make_inline_table(sdist)\n            if wheels:\n                result[\"wheels\"] = wheels\n\n            if package.dependencies is not None:\n                result[\"tool\"] = {\"pdm\": {\"dependencies\": make_array(package.dependencies, multiline=True)}}\n\n        return result\n\n    def _populate_hashes(self, packages: Iterable[Package]) -> None:\n        candidates: list[Candidate] = []\n        for package in packages:\n            if not package.candidate.req.is_named or package.candidate.req.extras:\n                continue\n            hashes = package.candidate.hashes\n            if all(\"url\" in hash_item for hash_item in hashes):\n                continue\n            package.candidate.hashes.clear()\n            candidates.append(package.candidate)\n        if candidates:\n            with self.project.core.ui.open_spinner(\"Fetching package file URLs\"):\n                repo = self.project.get_repository()\n                repo.fetch_hashes(candidates)\n\n    def convert(self, all_groups: Iterable[str] | None = None) -> dict[str, Any]:\n        doc = tomlkit.document()\n        project = self.project\n        lockfile = project.lockfile\n        if FLAG_INHERIT_METADATA not in lockfile.strategy:\n            raise ProjectError(\"inherit_metadata strategy is required for pylock format\")\n        repository = self.locked_repository\n        if all_groups is None:\n            all_groups = lockfile.groups\n        if all_groups is None:\n            all_groups = list(project.iter_groups())\n        extras, groups = self.project.split_extras_groups(list(all_groups))\n        env_markers: list[Marker] = []\n        for target in repository.targets:\n            env_markers.append(target.markers_with_python())\n\n        doc.update(\n            {\n                \"lock-version\": self.lock_version,\n                \"requires-python\": str(project.python_requires),\n                \"environments\": make_array([str(marker) for marker in env_markers], multiline=True),\n                \"extras\": sorted(extras),\n                \"dependency-groups\": sorted(groups, key=_group_sort_key),\n                \"default-groups\": [\"default\"],\n                \"created-by\": \"pdm\",\n            }\n        )\n        packages = doc.setdefault(\"packages\", tomlkit.aot())\n\n        with cd(project.root):\n            self._populate_hashes(repository.packages.values())\n            for package in repository.packages.values():\n                if package.candidate.req.extras:\n                    continue\n                package_table = self.make_package(package)\n                selection_markers: list[BaseMarker] = []\n                for item in sorted(package.candidate.req.groups, key=_group_sort_key):\n                    item = normalize_name(item)\n                    if item in extras:\n                        selection_markers.append(parse_marker(f\"'{item}' in extras\"))\n                    else:\n                        selection_markers.append(parse_marker(f\"'{item}' in dependency_groups\"))\n                marker = MarkerUnion.of(*selection_markers) if selection_markers else AnyMarker()\n                if package.candidate.req.marker is not None:\n                    marker = package.candidate.req.marker.inner & marker\n                if not marker.is_any():\n                    package_table[\"marker\"] = str(marker)\n                packages.append(package_table)\n\n        doc[\"tool\"] = {\n            \"pdm\": {\n                \"hashes\": make_inline_table({\"sha256\": project.pyproject.content_hash()}),\n                \"targets\": [spec.as_dict() for spec in repository.targets],\n            },\n        }\n\n        return doc\n"
  },
  {
    "path": "src/pdm/formats/requirements.py",
    "content": "from __future__ import annotations\n\nimport argparse\nimport hashlib\nimport shlex\nimport urllib.parse\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, Iterable, Mapping, cast\n\nfrom pdm.environments import BareEnvironment\nfrom pdm.exceptions import PdmException, PdmUsageError\nfrom pdm.formats.base import make_array\nfrom pdm.models.requirements import FileRequirement, Requirement, parse_requirement\n\nif TYPE_CHECKING:\n    from argparse import Namespace\n    from os import PathLike\n\n    from pdm.models.candidates import Candidate\n    from pdm.models.session import PDMPyPIClient\n    from pdm.project import Project\n\n\nclass RequirementParser:\n    \"\"\"Reference:\n    https://pip.pypa.io/en/stable/reference/requirements-file-format/\n    \"\"\"\n\n    # TODO: support no_binary, only_binary, prefer_binary, pre and no_index\n\n    def __init__(self, session: PDMPyPIClient) -> None:\n        self.requirements: list[Requirement] = []\n        self.index_url: str | None = None\n        self.extra_index_urls: list[str] = []\n        self.no_index: bool = False\n        self.find_links: list[str] = []\n        self.trusted_hosts: list[str] = []\n        parser = argparse.ArgumentParser()\n        parser.add_argument(\"--index-url\", \"-i\")\n        parser.add_argument(\"--no-index\", action=\"store_true\")\n        parser.add_argument(\"--extra-index-url\")\n        parser.add_argument(\"--find-links\", \"-f\")\n        parser.add_argument(\"--trusted-host\")\n        parser.add_argument(\"-e\", \"--editable\", nargs=\"+\")\n        parser.add_argument(\"-r\", \"--requirement\")\n        self._parser = parser\n        self._session = session\n\n    def _clean_line(self, line: str) -> str:\n        \"\"\"Strip the surrounding whitespaces and comment from the line\"\"\"\n        line = line.strip()\n        if line.startswith(\"#\"):\n            return \"\"\n        return line.split(\" #\", 1)[0].strip()\n\n    def _parse_line(self, filename: str, line: str) -> None:\n        if not line.startswith(\"-\"):\n            # Starts with a requirement, just ignore all per-requirement options\n            req_string = line.split(\" -\", 1)[0].strip()\n            req = parse_requirement(req_string)\n            if not req.name:\n                assert isinstance(req, FileRequirement)\n                req.name = req.guess_name()\n            self.requirements.append(req)\n            return\n        args, _ = self._parser.parse_known_args(shlex.split(line))\n        if args.index_url:\n            self.index_url = args.index_url\n        if args.no_index:\n            self.no_index = args.no_index\n        if args.extra_index_url:\n            self.extra_index_urls.append(args.extra_index_url)\n        if args.find_links:\n            self.find_links.append(args.find_links)\n        if args.trusted_host:\n            self.trusted_hosts.append(args.trusted_host)\n        if args.editable:\n            self.requirements.append(parse_requirement(\" \".join(args.editable), True))\n        if args.requirement:\n            referenced_requirements = Path(filename).parent.joinpath(args.requirement).as_posix()\n            self.parse_file(referenced_requirements)\n\n    def parse_lines(self, lines: Iterable[str], filename: str = \"<temp file>\") -> None:\n        this_line = \"\"\n        for line in filter(None, map(self._clean_line, lines)):\n            if line.endswith(\"\\\\\"):\n                this_line += line[:-1].rstrip() + \" \"\n                continue\n            this_line += line\n            self._parse_line(filename, this_line)\n            this_line = \"\"\n        if this_line:\n            self._parse_line(filename, this_line)\n\n    def parse_file(self, filename_or_url: str) -> None:\n        parsed = urllib.parse.urlparse(filename_or_url)\n        if parsed.scheme in (\"http\", \"https\", \"file\"):\n            resp = self._session.get(filename_or_url)\n            if resp.is_error:  # pragma: no cover\n                raise PdmException(\n                    f\"Failed to fetch {filename_or_url}: ({resp.status_code} - {resp.reason_phrase}) {resp.text}\"\n                )\n            return self.parse_lines(resp.text.splitlines(), filename_or_url)\n        with open(filename_or_url, encoding=\"utf-8\") as f:\n            self.parse_lines(f, filename_or_url)\n\n\ndef check_fingerprint(project: Project, filename: PathLike) -> bool:\n    from pdm.compat import tomllib\n\n    with open(filename, \"rb\") as fp:\n        try:\n            tomllib.load(fp)\n        except ValueError:\n            # the file should be a requirements.txt\n            # if it's not a TOML document nor py script.\n            return Path(filename).suffix not in (\".py\", \".cfg\")\n        else:\n            return False\n\n\ndef _is_url_trusted(url: str, trusted_hosts: list[str]) -> bool:\n    parsed = urllib.parse.urlparse(url)\n    netloc, host = parsed.netloc, parsed.hostname\n\n    for trusted in trusted_hosts:\n        if trusted in (host, netloc):\n            return True\n    return False\n\n\ndef convert_url_to_source(url: str, name: str | None, trusted_hosts: list[str], type: str = \"index\") -> dict[str, Any]:\n    if not name:\n        name = hashlib.sha1(url.encode(\"utf-8\")).hexdigest()[:6]\n    source = {\n        \"name\": name,\n        \"url\": url,\n        \"verify_ssl\": not _is_url_trusted(url, trusted_hosts),\n    }\n    if type != \"index\":\n        source[\"type\"] = type\n    return source\n\n\ndef convert(project: Project, filename: PathLike, options: Namespace) -> tuple[Mapping[str, Any], Mapping[str, Any]]:\n    env = BareEnvironment(project)\n    parser = RequirementParser(env.session)\n    parser.parse_file(str(filename))\n    backend = project.backend\n\n    deps = make_array([], True)\n    dev_deps = make_array([], True)\n\n    for req in parser.requirements:\n        if req.is_file_or_url:\n            req.relocate(backend)  # type: ignore[attr-defined]\n        if req.editable or options.dev:\n            dev_deps.append(req.as_line())\n        else:\n            deps.append(req.as_line())\n    data: dict[str, Any] = {}\n    settings: dict[str, Any] = {}\n    if dev_deps:\n        dev_group = options.group if options.group and options.dev else \"dev\"\n        settings[\"dev-dependencies\"] = {dev_group: dev_deps}\n    if options.group and deps:\n        data[\"optional-dependencies\"] = {options.group: deps}\n    else:\n        data[\"dependencies\"] = deps\n    sources: list[dict[str, Any]] = []\n    if parser.index_url and not parser.no_index:\n        sources.append(convert_url_to_source(parser.index_url, \"pypi\", parser.trusted_hosts))\n    if not parser.no_index:\n        for url in parser.extra_index_urls:\n            sources.append(convert_url_to_source(url, None, parser.trusted_hosts))\n    if parser.find_links:\n        first, *find_links = parser.find_links\n        sources.append(\n            convert_url_to_source(\n                first,\n                \"pypi\" if parser.no_index else None,\n                parser.trusted_hosts,\n                \"find_links\",\n            )\n        )\n        for url in find_links:\n            sources.append(convert_url_to_source(url, None, parser.trusted_hosts, \"find_links\"))\n\n    if sources:\n        settings[\"source\"] = sources\n    return data, settings\n\n\ndef export(\n    project: Project,\n    candidates: list[Candidate] | list[Requirement],\n    options: Namespace,\n) -> str:\n    from pdm.models.candidates import Candidate\n\n    lines = [\"# This file is @generated by PDM.\\n# Please do not edit it manually.\\n\\n\"]\n    collected_req: set[str] = set()\n    for candidate in sorted(candidates, key=lambda x: x.identify()):  # type: ignore[attr-defined]\n        if isinstance(candidate, Candidate):\n            req = candidate.req.as_pinned_version(candidate.version)\n            if options.hashes and not candidate.hashes:\n                raise PdmUsageError(f\"Hash is not available for '{req}', please export with `--no-hashes` option.\")\n        else:\n            assert isinstance(candidate, Requirement)\n            req = candidate\n        line = project.backend.expand_line(req.as_line(), options.expandvars)\n        if line in collected_req:\n            continue\n        lines.append(project.backend.expand_line(req.as_line(), options.expandvars))\n        collected_req.add(line)\n        if options.hashes and getattr(candidate, \"hashes\", None):\n            for item in sorted({row[\"hash\"] for row in candidate.hashes}):  # type: ignore[union-attr]\n                lines.append(f\" \\\\\\n    --hash={item}\")\n        lines.append(\"\\n\")\n    if (options.self or options.editable_self) and not project.is_distribution:\n        raise PdmUsageError(\"Cannot export the project itself in a non-library project.\")\n    if options.hashes and (options.self or options.editable_self):\n        raise PdmUsageError(\"Hash is not available for `--self/--editable-self`. Please export with `--no-hashes`.\")\n    if options.self:\n        lines.append(\".  # this package\\n\")\n    elif options.editable_self:\n        lines.append(\"-e .  # this package\\n\")\n\n    for source in project.get_sources(expand_env=options.expandvars, include_stored=False):\n        url = cast(str, source.url)\n        source_type = source.type or \"index\"\n        if source_type == \"index\":\n            prefix = \"--index-url\" if source.name == \"pypi\" else \"--extra-index-url\"\n        elif source_type == \"find_links\":\n            prefix = \"--find-links\"\n        else:\n            raise ValueError(f\"Unknown source type: {source_type}\")\n        lines.append(f\"{prefix} {url}\\n\")\n        if source.verify_ssl is False:\n            host = urllib.parse.urlparse(url).hostname\n            lines.append(f\"--trusted-host {host}\\n\")\n    return \"\".join(lines)\n"
  },
  {
    "path": "src/pdm/formats/setup_py.py",
    "content": "from __future__ import annotations\n\nimport os\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, Mapping\n\nfrom pdm.formats.base import array_of_inline_tables, make_array, make_inline_table\n\nif TYPE_CHECKING:\n    from pdm.project import Project\n\n\ndef check_fingerprint(project: Project, filename: Path) -> bool:\n    return os.path.basename(filename) in (\"setup.py\", \"setup.cfg\")\n\n\ndef convert(project: Project, filename: Path, options: Any | None) -> tuple[Mapping[str, Any], Mapping[str, Any]]:\n    from pdm.models.in_process import parse_setup_py\n\n    parsed = parse_setup_py(str(project.environment.interpreter.executable), os.path.dirname(filename))\n    metadata: dict[str, Any] = {}\n    settings: dict[str, Any] = {}\n    for name in [\n        \"name\",\n        \"version\",\n        \"description\",\n        \"keywords\",\n        \"urls\",\n        \"readme\",\n    ]:\n        if name in parsed:\n            metadata[name] = parsed[name]\n    if \"authors\" in parsed:\n        metadata[\"authors\"] = array_of_inline_tables(parsed[\"authors\"])\n    if \"maintainers\" in parsed:\n        metadata[\"maintainers\"] = array_of_inline_tables(parsed[\"maintainers\"])\n    if \"classifiers\" in parsed:\n        metadata[\"classifiers\"] = make_array(sorted(parsed[\"classifiers\"]), True)\n    if \"python_requires\" in parsed:\n        metadata[\"requires-python\"] = parsed[\"python_requires\"]\n    if \"install_requires\" in parsed:\n        metadata[\"dependencies\"] = make_array(sorted(parsed[\"install_requires\"]), True)\n    if \"extras_require\" in parsed:\n        metadata[\"optional-dependencies\"] = {\n            k: make_array(sorted(v), True) for k, v in parsed[\"extras_require\"].items()\n        }\n    if \"license\" in parsed:\n        metadata[\"license\"] = make_inline_table({\"text\": parsed[\"license\"]})\n    if \"package_dir\" in parsed:\n        settings[\"package-dir\"] = parsed[\"package_dir\"]\n\n    entry_points = parsed.get(\"entry_points\", {})\n    if \"console_scripts\" in entry_points:\n        metadata[\"scripts\"] = entry_points.pop(\"console_scripts\")\n    if \"gui_scripts\" in entry_points:\n        metadata[\"gui-scripts\"] = entry_points.pop(\"gui_scripts\")\n    if entry_points:\n        metadata[\"entry-points\"] = entry_points\n    # reset the environment as `requires-python` may change\n    project.environment = None  # type: ignore[assignment]\n    return metadata, settings\n\n\ndef export(project: Project, candidates: list, options: Any | None) -> str:\n    raise NotImplementedError()\n"
  },
  {
    "path": "src/pdm/formats/uv.py",
    "content": "from __future__ import annotations\n\nimport tempfile\nfrom collections.abc import Iterator\nfrom contextlib import ExitStack, contextmanager\nfrom dataclasses import dataclass, field\nfrom functools import cached_property\nfrom pathlib import Path\nfrom typing import Any, cast\n\nimport tomlkit\n\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.markers import Marker, get_marker\nfrom pdm.models.repositories import LockedRepository, Package\nfrom pdm.models.requirements import FileRequirement, Requirement, VcsRequirement, parse_requirement, strip_extras\nfrom pdm.project.core import Project\nfrom pdm.utils import get_requirement_from_override, normalize_name\n\n\n@dataclass\nclass _UvFileBuilder:\n    project: Project\n    requires_python: str\n    requirements: list[Requirement]\n    locked_repository: LockedRepository\n    stack: ExitStack = field(default_factory=ExitStack, init=False)\n\n    @cached_property\n    def default_source(self) -> str:\n        return cast(str, self.project.sources[0].url)\n\n    def __post_init__(self) -> None:\n        self._enter_path(self.project.root / \"uv.lock\")\n\n    def build_pyproject_toml(self) -> Path:\n        data = self.project.pyproject.open_for_read()\n\n        uv_overrides = []\n        for override_key, override_value in (\n            data.get(\"tool\", {}).get(\"pdm\", {}).get(\"resolution\", {}).get(\"overrides\", {}).items()\n        ):\n            uv_overrides.append(f\"{get_requirement_from_override(override_key, override_value)}\")\n\n        if uv_overrides:\n            data.setdefault(\"tool\", {}).setdefault(\"uv\", {}).setdefault(\"override-dependencies\", []).extend(\n                uv_overrides\n            )\n\n        data.setdefault(\"project\", {})[\"requires-python\"] = self.requires_python\n        data.pop(\"dependency-groups\", None)\n        data.setdefault(\"project\", {}).pop(\"optional-dependencies\", None)\n        sources = {}\n        collected_deps: dict[str, list[str]] = {}\n        for dep in self.requirements:\n            if isinstance(dep, FileRequirement):\n                entry = self._get_name(dep)\n                sources[entry] = self._build_source(dep)\n            else:\n                entry = dep.as_line()\n            for group in dep.groups:\n                collected = collected_deps.setdefault(group, [])\n                if entry not in collected:\n                    collected.append(entry)\n\n        for group, deps in collected_deps.items():\n            if group == \"default\":\n                data.setdefault(\"project\", {})[\"dependencies\"] = deps\n            else:\n                data.setdefault(\"project\", {}).setdefault(\"optional-dependencies\", {})[group] = deps\n\n        if sources:\n            data.setdefault(\"tool\", {}).setdefault(\"uv\", {}).setdefault(\"sources\", {}).update(sources)\n\n        path = self._enter_path(self.project.root / \"pyproject.toml\")\n        with path.open(\"w\", newline=\"\", encoding=\"utf-8\") as f:\n            tomlkit.dump(data, f)\n        return path\n\n    def _enter_path(self, path: Path) -> Path:\n        if path.exists():\n            name = tempfile.mktemp(dir=path.parent, prefix=f\"{path.name}.\")\n            backup = path.rename(name)\n\n            @self.stack.callback\n            def restore() -> None:\n                path.unlink(True)\n                backup.rename(path)\n        else:\n            self.stack.callback(path.unlink, True)\n        return path\n\n    def build_uv_lock(self, include_self: bool = False) -> Path:\n        locked_repo = self.locked_repository\n        packages: list[dict[str, Any]] = []\n        for key in locked_repo.packages:\n            if \"[\" in key[0]:  # skip entries with extras\n                continue\n            # Merge related entries with the same name and version\n            related_packages = [\n                p for k, p in locked_repo.packages.items() if strip_extras(k[0])[0] == key[0] and k[1:] == key[1:]\n            ]\n            packages.append(self._build_lock_entry(related_packages))\n        if name := self.project.name:\n            version = self.project.pyproject.metadata.get(\"version\", \"0.0.0\")\n            this_package = {\n                \"name\": normalize_name(name),\n                \"version\": version,\n                \"source\": {\"editable\" if include_self else \"virtual\": \".\"},\n            }\n            dependencies: list[dict[str, Any]] = []\n            optional_dependencies: dict[str, list[dict[str, Any]]] = {}\n            for req in self.requirements:\n                if (dep := self._make_dependency(None, req)) is None:\n                    continue\n                for group in req.groups:\n                    if group == \"default\":\n                        target_group = dependencies\n                    else:\n                        target_group = optional_dependencies.setdefault(group, [])\n                    if dep not in target_group:\n                        target_group.append(dep)\n            if dependencies:\n                this_package[\"dependencies\"] = dependencies  # type: ignore[assignment]\n            if optional_dependencies:\n                this_package[\"optional-dependencies\"] = optional_dependencies\n            packages.append(this_package)\n\n        data = {\"version\": 1, \"requires-python\": self.requires_python}\n        if packages:\n            data[\"package\"] = packages\n        path = self.project.root / \"uv.lock\"\n        with path.open(\"w\", newline=\"\", encoding=\"utf-8\") as f:\n            tomlkit.dump(data, f)\n        return path\n\n    def _get_name(self, req: FileRequirement) -> str:\n        if req.key:\n            return req.key\n        can = Candidate(req).prepare(self.project.environment)\n        return normalize_name(can.metadata.name)\n\n    def _build_source(self, req: FileRequirement) -> dict[str, Any]:\n        result: dict[str, Any]\n        if isinstance(req, VcsRequirement):\n            result = {req.vcs: req.repo}\n            if req.ref:\n                result[\"rev\"] = req.ref\n        elif req.path:\n            result = {\"path\": req.str_path}\n        else:\n            result = {\"url\": req.url}\n        if req.editable:\n            result[\"editable\"] = True\n        return result\n\n    def _build_lock_source(self, req: Requirement) -> dict[str, Any]:\n        if isinstance(req, VcsRequirement):\n            return {req.vcs: f\"{req.repo}?rev={req.ref}#{req.revision}\"}\n        elif isinstance(req, FileRequirement):\n            if req.editable:\n                return {\"editable\": req.str_path}\n            else:\n                return {\"url\": req.url}\n        else:\n            return {\"registry\": self.default_source}\n\n    def _build_lock_entry(self, packages: list[Package]) -> dict[str, Any]:\n        packages.sort(key=lambda x: len(x.candidate.req.extras or []))\n        candidate = packages[0].candidate\n        req = candidate.req\n        result: dict[str, Any] = {\n            \"name\": candidate.name,\n            \"version\": candidate.version,\n            \"source\": self._build_lock_source(req),\n        }\n        for file_hash in candidate.hashes:\n            filename = file_hash.get(\"url\", file_hash.get(\"file\", \"\"))\n            is_wheel = filename.endswith(\".whl\")\n            item = {\"url\": file_hash.get(\"url\", filename), \"hash\": file_hash[\"hash\"]}\n            if is_wheel:\n                result.setdefault(\"wheels\", []).append(item)\n            else:\n                result[\"sdist\"] = item\n        optional_dependencies: dict[str, list[dict[str, Any]]] = {}\n        for package in packages:\n            if package.dependencies is None:\n                continue\n            if not package.candidate.req.extras:\n                deps = [\n                    self._make_dependency(package.candidate, parse_requirement(dep)) for dep in package.dependencies\n                ]\n                result[\"dependencies\"] = [dep for dep in deps if dep is not None]\n            else:\n                deps = [\n                    self._make_dependency(package.candidate, parse_requirement(dep))\n                    for dep in package.dependencies\n                    if parse_requirement(dep).key != candidate.req.key\n                ]\n                deps = [dep for dep in deps if dep is not None]\n                for extra in package.candidate.req.extras:\n                    # XXX: when depending on a package with extras, the extra dependencies are encoded in\n                    # the corresponding group under optional-dependencies. But in case multiple extras are requested,\n                    # the same dependencies get duplicated in those groups, but it's okay if each single extra is\n                    # never requested alone.\n                    if extra not in optional_dependencies:\n                        optional_dependencies[extra] = deps  # type: ignore[assignment]\n\n        if optional_dependencies:\n            result[\"optional-dependencies\"] = optional_dependencies\n        return result\n\n    def _make_dependency(self, parent: Candidate | None, req: Requirement) -> dict[str, Any] | None:\n        locked_repo = self.locked_repository\n        parent_marker = req.marker or get_marker(\"\")\n        if parent is not None:\n            parent_marker &= parent.req.marker or get_marker(\"\")\n        matching_entries = [e for k, e in locked_repo.packages.items() if k[0] == req.key]\n\n        def marker_match(marker: Marker | None) -> bool:\n            return not (parent_marker & (marker or get_marker(\"\"))).is_empty()\n\n        if not matching_entries:\n            return None\n        result: dict[str, Any] = {}\n        if len(matching_entries) == 1:\n            candidate = matching_entries[0].candidate\n            multiple = False\n        else:\n            candidate = next(e.candidate for e in matching_entries if marker_match(e.candidate.req.marker))\n            multiple = True\n        result.update({\"name\": candidate.name})\n        if multiple:\n            result.update(version=candidate.version, source=self._build_lock_source(candidate.req))\n        if req.extras:\n            result[\"extra\"] = list(req.extras)\n        if req.marker is not None:\n            result[\"marker\"] = str(req.marker)\n        return result\n\n\n@contextmanager\ndef uv_file_builder(\n    project: Project, requires_python: str, requirements: list[Requirement], locked_repository: LockedRepository\n) -> Iterator[_UvFileBuilder]:\n    builder = _UvFileBuilder(project, requires_python, requirements, locked_repository)\n    with builder.stack:\n        yield builder\n"
  },
  {
    "path": "src/pdm/installers/__init__.py",
    "content": "from pdm.installers.base import BaseSynchronizer\nfrom pdm.installers.manager import InstallManager\nfrom pdm.installers.synchronizers import Synchronizer\nfrom pdm.installers.uv import UvSynchronizer\n\n__all__ = [\"BaseSynchronizer\", \"InstallManager\", \"Synchronizer\", \"UvSynchronizer\"]\n"
  },
  {
    "path": "src/pdm/installers/base.py",
    "content": "from __future__ import annotations\n\nimport dataclasses\nimport json\nfrom functools import cached_property\nfrom itertools import chain\nfrom typing import Collection, Iterable\n\nfrom pdm import termui\nfrom pdm.compat import Distribution\nfrom pdm.environments import BaseEnvironment\nfrom pdm.exceptions import BuildError\nfrom pdm.installers.manager import InstallManager\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.repositories import Package\nfrom pdm.models.requirements import FileRequirement, Requirement, parse_requirement, strip_extras\nfrom pdm.utils import is_editable, normalize_name\n\n\ndef editables_candidate(environment: BaseEnvironment) -> Candidate | None:\n    \"\"\"Return a candidate for `editables` package\"\"\"\n    with environment.get_finder() as finder:\n        best = finder.find_best_match(\"editables\").best\n    return None if best is None else Candidate.from_installation_candidate(best, parse_requirement(\"editables\"))\n\n\nclass BaseSynchronizer:\n    \"\"\"Synchronize the working set with given installation candidates\n\n    :param candidates: a dict of candidates to be installed\n    :param environment: the environment associated with the project\n    :param clean: clean unneeded packages\n    :param dry_run: only prints summary but do not install or uninstall\n    :param retry_times: retry times when installation failed\n    :param install_self: whether to install self project\n    :param no_editable: if True, override all editable installations,\n        if a list, override editables with the given names\n    :param use_install_cache: whether to use install cache\n    :param reinstall: whether to reinstall all packages\n    :param only_keep: If true, only keep the selected candidates\n    :param fail_fast: If true, stop the installation on first error\n    \"\"\"\n\n    SEQUENTIAL_PACKAGES = (\"pip\", \"setuptools\", \"wheel\")\n\n    def __init__(\n        self,\n        environment: BaseEnvironment,\n        candidates: dict[str, Candidate] | None = None,\n        clean: bool = False,\n        dry_run: bool = False,\n        retry_times: int = 1,\n        install_self: bool = False,\n        no_editable: bool | Collection[str] = False,\n        reinstall: bool = False,\n        only_keep: bool = False,\n        fail_fast: bool = False,\n        use_install_cache: bool | None = None,\n        packages: Iterable[Package] = (),\n        requirements: Iterable[Requirement] | None = None,\n    ) -> None:\n        if candidates:  # pragma: no cover\n            self.requested_candidates = candidates\n        else:\n            self.requested_candidates = {entry.candidate.identify(): entry.candidate for entry in packages}\n        self.environment = environment\n        self.clean = clean\n        self.dry_run = dry_run\n        self.retry_times = retry_times\n        self.no_editable = no_editable\n        self.install_self = install_self\n        if use_install_cache is None:\n            use_install_cache = bool(environment.project.config[\"install.cache\"])\n        self.use_install_cache = use_install_cache\n        self.reinstall = reinstall\n        self.only_keep = only_keep\n        self.parallel = environment.project.config[\"install.parallel\"]\n        self.fail_fast = fail_fast\n\n        self.working_set = environment.get_working_set()\n        self.ui = environment.project.core.ui\n        self._manager: InstallManager | None = None\n        self.packages = packages\n        self.requirements = requirements\n\n    @cached_property\n    def self_candidate(self) -> Candidate:\n        \"\"\"Return the candidate for self project\"\"\"\n        return self.environment.project.make_self_candidate(not self.no_editable)\n\n    @cached_property\n    def candidates(self) -> dict[str, Candidate]:\n        \"\"\"Return the candidates to be installed\"\"\"\n        candidates = self.requested_candidates.copy()\n        requested = {\n            req.identify()\n            for req in (self.requirements or chain.from_iterable(self.environment.project.all_dependencies.values()))\n        }\n        if isinstance(self.no_editable, Collection):\n            keys = self.no_editable\n        elif self.no_editable:\n            keys = candidates.keys()\n        else:\n            keys = []\n            if self.should_install_editables():\n                # Install `editables` as well as required by self project\n                editables = editables_candidate(self.environment)\n                if editables is not None:\n                    candidates[\"editables\"] = editables\n        for key in keys:\n            if key in candidates and candidates[key].req.editable:\n                candidate = candidates[key]\n                # Create a new candidate with editable=False\n                req = dataclasses.replace(candidate.req, editable=False)\n                candidates[key] = candidate.copy_with(req)\n        for key in requested:\n            if key in candidates:\n                candidates[key].requested = True\n        return candidates\n\n    def should_install_editables(self) -> bool:\n        \"\"\"Return whether to add editables\"\"\"\n        if not self.install_self or \"editables\" in self.requested_candidates:\n            return False\n        # As editables may be added by the backend, we need to check the metadata\n        try:\n            metadata = self.self_candidate.prepare(self.environment).metadata\n        except BuildError:\n            return False\n        return any(req.startswith(\"editables\") for req in metadata.requires or [])\n\n    @property\n    def manager(self) -> InstallManager:\n        if not self._manager:\n            self._manager = self.get_manager(rename_pth=True)\n        return self._manager\n\n    def get_manager(self, rename_pth: bool = False) -> InstallManager:\n        return self.environment.project.core.install_manager_class(\n            self.environment, use_install_cache=self.use_install_cache, rename_pth=rename_pth\n        )\n\n    @property\n    def self_key(self) -> str | None:\n        if not self.install_self:\n            return None\n        name = self.environment.project.name\n        if name:\n            return normalize_name(name)\n        return name\n\n    def _should_update(self, dist: Distribution, can: Candidate) -> bool:\n        \"\"\"Check if the candidate should be updated\"\"\"\n        backend = self.environment.project.backend\n        if self.reinstall or can.req.editable:  # Always update if incoming is editable\n            return True\n        if is_editable(dist):  # only update editable if no_editable is True\n            return bool(self.no_editable)\n        if not can.req.is_named:\n            dreq = Requirement.from_dist(dist)\n            if not isinstance(dreq, FileRequirement):\n                return True\n            url = dreq.get_full_url()\n            if dreq.is_local_dir:\n                # We don't know whether a local dir has been changed, always update\n                return True\n            assert can.link is not None\n            if url != backend.expand_line(can.link.url_without_fragment):\n                return True\n            direct_json = json.loads(content) if (content := dist.read_text(\"direct_url.json\")) else None\n            if not direct_json or \"archive_info\" not in direct_json:\n                # We are not able to check, don't update\n                return False\n            dist_hash = direct_json[\"archive_info\"][\"hash\"].replace(\"=\", \":\")\n            return not any(dist_hash == file_hash[\"hash\"] for file_hash in can.hashes)\n        specifier = can.req.as_pinned_version(can.version).specifier\n        return not specifier.contains(dist.version, prereleases=True)\n\n    def compare_with_working_set(self) -> tuple[list[str], list[str], list[str]]:\n        \"\"\"Compares the candidates and return (to_add, to_update, to_remove)\"\"\"\n        working_set = self.working_set\n        candidates = self.candidates.copy()\n        to_update: set[str] = set()\n        to_remove: set[str] = set()\n        to_add: set[str] = set()\n        locked_repository = self.environment.project.get_locked_repository()\n        all_candidate_keys = list(locked_repository.all_candidates)\n\n        for key, dist in working_set.items():\n            if key == self.self_key and self.install_self:\n                continue\n            if key in candidates:\n                can = candidates.pop(key)\n                if self._should_update(dist, can):\n                    if working_set.is_owned(key):\n                        to_update.add(key)\n                    else:\n                        to_add.add(key)\n            elif (\n                (self.only_keep or (self.clean and key not in all_candidate_keys))\n                and key not in self.SEQUENTIAL_PACKAGES\n                and working_set.is_owned(key)\n            ):\n                # Remove package only if it is not required by any group\n                # Packages for packaging will never be removed\n                to_remove.add(key)\n        to_add.update(\n            strip_extras(name)[0]\n            for name, _ in candidates.items()\n            if name != self.self_key and strip_extras(name)[0] not in working_set\n        )\n        return (sorted(to_add), sorted(to_update), sorted(to_remove))\n\n    def synchronize(self) -> None:\n        \"\"\"Synchronize the working set with pinned candidates.\"\"\"\n        to_add, to_update, to_remove = self.compare_with_working_set()\n        manager = self.get_manager()\n        for key in to_add:\n            can = self.candidates[key]\n            termui.logger.info(\"Installing %s@%s...\", key, can.version)\n            manager.install(can)\n        for key in to_update:\n            can = self.candidates[key]\n            dist = self.working_set[strip_extras(key)[0]]\n            dist_version = dist.version\n            termui.logger.info(\"Updating %s@%s -> %s...\", key, dist_version, can.version)\n            manager.overwrite(dist, can)\n        for key in to_remove:\n            dist = self.working_set[key]\n            termui.logger.info(\"Removing %s@%s...\", key, dist.version)\n            manager.uninstall(dist)\n        if self.install_self:\n            self_key = self.self_key\n            assert self_key\n            word = \"a\" if self.no_editable else \"an editable\"\n            termui.logger.info(f\"Installing the project as {word} package...\")\n            if self_key in self.working_set:\n                dist = self.working_set[strip_extras(self_key)[0]]\n                manager.overwrite(dist, self.self_candidate)\n            else:\n                manager.install(self.self_candidate)\n        termui.logger.info(\"Synchronization complete.\")\n"
  },
  {
    "path": "src/pdm/installers/core.py",
    "content": "from __future__ import annotations\n\nfrom typing import Iterable\n\nfrom pdm.environments import BaseEnvironment\nfrom pdm.models.requirements import Requirement\nfrom pdm.resolver.reporters import LockReporter\n\n\ndef install_requirements(\n    reqs: Iterable[Requirement],\n    environment: BaseEnvironment,\n    clean: bool = False,\n    use_install_cache: bool = False,\n    allow_uv: bool = True,\n) -> None:  # pragma: no cover\n    \"\"\"Resolve and install the given requirements into the environment.\"\"\"\n    reqs = [req for req in reqs if not req.marker or req.marker.matches(environment.spec)]\n    reporter = LockReporter()\n    project = environment.project\n    backend = project.backend\n    for req in reqs:\n        if req.is_file_or_url:\n            req.relocate(backend)  # type: ignore[attr-defined]\n    resolver = project.get_resolver(allow_uv=allow_uv)(\n        environment=environment,\n        requirements=reqs,\n        update_strategy=\"all\",\n        strategies=project.lockfile.default_strategies,\n        target=environment.spec,\n        tracked_names=(),\n        keep_self=True,\n        reporter=reporter,\n    )\n    resolved = resolver.resolve().packages\n    syncer = environment.project.get_synchronizer(quiet=True, allow_uv=allow_uv)(\n        environment,\n        clean=clean,\n        retry_times=0,\n        install_self=False,\n        use_install_cache=use_install_cache,\n        packages=resolved,\n        requirements=reqs,\n    )\n    syncer.synchronize()\n"
  },
  {
    "path": "src/pdm/installers/installers.py",
    "content": "from __future__ import annotations\n\nimport json\nimport os\nimport stat\nfrom functools import cached_property\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Iterator\n\nfrom installer import install as _install\nfrom installer._core import _process_WHEEL_file\nfrom installer.destinations import SchemeDictionaryDestination, WheelDestination\nfrom installer.exceptions import InvalidWheelSource\nfrom installer.records import RecordEntry\nfrom installer.sources import WheelContentElement, WheelSource\nfrom installer.sources import WheelFile as _WheelFile\n\nfrom pdm.models.cached_package import CachedPackage\n\nif TYPE_CHECKING:\n    from typing import Any, BinaryIO, Iterable, Literal\n\n    from installer.destinations import Scheme\n    from installer.sources import WheelContentElement\n\n    from pdm.environments import BaseEnvironment\n\n    LinkMethod = Literal[\"symlink\", \"hardlink\", \"copy\"]\n\n\nclass WheelFile(_WheelFile):\n    @cached_property\n    def dist_info_dir(self) -> str:\n        namelist = self._zipfile.namelist()\n        try:\n            return next(name.split(\"/\")[0] for name in namelist if name.split(\"/\")[0].endswith(\".dist-info\"))\n        except StopIteration:  # pragma: no cover\n            canonical_name = super().dist_info_dir\n            raise InvalidWheelSource(f\"The wheel doesn't contain metadata {canonical_name!r}\") from None\n\n\nclass PackageWheelSource(WheelSource):\n    def __init__(self, package: CachedPackage) -> None:\n        self.package = package\n\n        distribution, version = package.path.name.split(\"-\")[:2]\n        super().__init__(distribution, version)\n\n    @cached_property\n    def dist_info_dir(self) -> str:\n        return self.package.dist_info.name\n\n    @property\n    def dist_info_filenames(self) -> list[str]:\n        return os.listdir(self.package.dist_info)\n\n    def read_dist_info(self, filename: str) -> str:\n        return self.package.dist_info.joinpath(filename).read_text(\"utf-8\")\n\n    def iter_files(self) -> Iterable[Path]:\n        for root, _, files in os.walk(self.package.path):\n            for file in files:\n                if Path(root) == self.package.path and file in CachedPackage.cache_files:\n                    continue\n                yield Path(root, file)\n\n    def get_contents(self) -> Iterator[WheelContentElement]:\n        from installer.records import parse_record_file\n\n        record_lines = self.read_dist_info(\"RECORD\").splitlines()\n        records = parse_record_file(record_lines)\n        record_mapping = {record[0]: record for record in records}\n\n        for item in self.iter_files():\n            fn = item.relative_to(self.package.path).as_posix()\n\n            # Pop record with empty default, because validation is handled by `validate_record`\n            record = record_mapping.pop(fn, (fn, \"\", \"\"))\n\n            # Borrowed from:\n            # https://github.com/pypa/pip/blob/0f21fb92/src/pip/_internal/utils/unpacking.py#L96-L100\n            mode = item.stat().st_mode\n            is_executable = bool(mode and stat.S_ISREG(mode) and mode & 0o111)\n\n            with item.open(\"rb\") as stream:\n                yield record, stream, is_executable\n\n\nclass InstallDestination(SchemeDictionaryDestination):\n    def __init__(\n        self,\n        *args: Any,\n        link_method: LinkMethod = \"copy\",\n        rename_pth: bool = False,\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(*args, **kwargs)\n        self.link_method = link_method\n        self.rename_pth = rename_pth\n\n    def write_to_fs(self, scheme: Scheme, path: str, stream: BinaryIO, is_executable: bool) -> RecordEntry:\n        from installer.records import Hash\n        from installer.utils import copyfileobj_with_hashing, make_file_executable\n\n        target_path = os.path.join(self.scheme_dict[scheme], path)\n        if os.path.exists(target_path):\n            os.unlink(target_path)\n\n        os.makedirs(os.path.dirname(target_path), exist_ok=True)\n\n        if self.rename_pth and target_path.endswith(\".pth\") and \"/\" not in path:\n            # Postpone the creation of pth files since it may cause race condition\n            # when multiple packages are installed at the same time.\n            target_path += \".pdmtmp\"\n        if self.link_method == \"copy\" or not hasattr(stream, \"name\"):\n            with open(target_path, \"wb\") as f:\n                hash_, size = copyfileobj_with_hashing(stream, f, self.hash_algorithm)\n        else:\n            src_path = stream.name\n            # create links, we don't need the stream anymore\n            stream.close()\n            if self.link_method == \"symlink\":\n                os.symlink(src_path, target_path)\n            else:  # hardlink\n                os.link(src_path, target_path)\n            hash_ = \"\"\n            size = os.path.getsize(target_path)\n\n        if is_executable:\n            make_file_executable(target_path)\n        return RecordEntry(path, Hash(self.hash_algorithm, hash_), size)\n\n\ndef _get_link_method(cache_method: str) -> LinkMethod:\n    from pdm import utils\n\n    if \"symlink\" in cache_method and utils.fs_supports_link_method(\"symlink\"):\n        return \"symlink\"\n\n    if \"link\" in cache_method and utils.fs_supports_link_method(\"link\"):\n        return \"hardlink\"\n    return \"copy\"\n\n\ndef install_wheel(\n    wheel: Path,\n    environment: BaseEnvironment,\n    direct_url: dict[str, Any] | None = None,\n    install_links: bool = False,\n    rename_pth: bool = False,\n    requested: bool = False,\n) -> str:\n    \"\"\"Only create .pth files referring to the cached package.\n    If the cache doesn't exist, create one.\n    \"\"\"\n    interpreter = str(environment.interpreter.executable)\n    script_kind = environment.script_kind\n    # the cache_method can be any of \"symlink\", \"hardlink\", \"copy\" and \"pth\"\n    cache_method: str = environment.project.config[\"install.cache_method\"]\n    dist_name = wheel.name.split(\"-\")[0]\n    link_method: LinkMethod | None\n    if not install_links or dist_name == \"editables\":\n        link_method = \"copy\"\n    else:\n        link_method = _get_link_method(cache_method)\n\n    additional_metadata: dict[str, bytes] = {\"INSTALLER\": b\"pdm\"}\n    if direct_url is not None:\n        additional_metadata[\"direct_url.json\"] = json.dumps(direct_url, indent=2).encode()\n    if requested:\n        additional_metadata[\"REQUESTED\"] = b\"\"\n\n    destination = InstallDestination(\n        scheme_dict=environment.get_paths(dist_name),\n        interpreter=interpreter,\n        script_kind=script_kind,\n        link_method=link_method,\n        rename_pth=rename_pth,\n    )\n    if install_links:\n        package = environment.project.package_cache.cache_wheel(wheel)\n        source = PackageWheelSource(package)\n        if link_method == \"symlink\":\n            # Track usage when symlink is used\n            additional_metadata[\"REFER_TO\"] = package.path.as_posix().encode()\n        dist_info_dir = install(source, destination=destination, additional_metadata=additional_metadata)\n        if link_method == \"symlink\":\n            package.add_referrer(dist_info_dir)\n    else:\n        with WheelFile.open(wheel) as source:\n            dist_info_dir = install(source, destination=destination, additional_metadata=additional_metadata)\n    return dist_info_dir\n\n\ndef install(\n    source: WheelSource, destination: WheelDestination, additional_metadata: dict[str, bytes] | None = None\n) -> str:\n    \"\"\"A lower level installation method that is copied from installer\n    but is controlled by extra parameters.\n\n    Return the .dist-info path\n    \"\"\"\n    _install(source, destination, additional_metadata=additional_metadata or {})\n    root_scheme = _process_WHEEL_file(source)\n    return os.path.join(destination.scheme_dict[root_scheme], source.dist_info_dir)\n"
  },
  {
    "path": "src/pdm/installers/manager.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom pdm import termui\nfrom pdm.compat import Distribution\nfrom pdm.exceptions import UninstallError\nfrom pdm.installers.installers import install_wheel\nfrom pdm.installers.uninstallers import BaseRemovePaths, StashedRemovePaths\n\nif TYPE_CHECKING:\n    from pdm.environments import BaseEnvironment\n    from pdm.models.candidates import Candidate\n\n\nclass InstallManager:\n    \"\"\"The manager that performs the installation and uninstallation actions.\"\"\"\n\n    # The packages below are needed to load paths and thus should not be cached.\n    NO_CACHE_PACKAGES = (\"editables\",)\n\n    def __init__(\n        self, environment: BaseEnvironment, *, use_install_cache: bool = False, rename_pth: bool = False\n    ) -> None:\n        self.environment = environment\n        self.use_install_cache = use_install_cache\n        self.rename_pth = rename_pth\n\n    def install(self, candidate: Candidate) -> Distribution:\n        \"\"\"Install a candidate into the environment, return the distribution\"\"\"\n        prepared = candidate.prepare(self.environment)\n        dist_info = install_wheel(\n            prepared.build(),\n            self.environment,\n            direct_url=prepared.direct_url(),\n            install_links=self.use_install_cache and not candidate.req.editable,\n            rename_pth=self.rename_pth,\n            requested=candidate.requested,\n        )\n        return Distribution.at(dist_info)\n\n    def get_paths_to_remove(self, dist: Distribution) -> BaseRemovePaths:\n        \"\"\"Get the path collection to be removed from the disk\"\"\"\n        return StashedRemovePaths.from_dist(dist, environment=self.environment)\n\n    def uninstall(self, dist: Distribution) -> None:\n        \"\"\"Perform the uninstallation for a given distribution\"\"\"\n        remove_path = self.get_paths_to_remove(dist)\n        dist_name = dist.metadata.get(\"Name\")\n        termui.logger.info(\"Removing distribution %s\", dist_name)\n        try:\n            remove_path.remove()\n            remove_path.commit()\n        except OSError as e:\n            termui.logger.warning(\"Error occurred during uninstallation, roll back the changes now.\")\n            remove_path.rollback()\n            raise UninstallError(e) from e\n\n    def overwrite(self, dist: Distribution, candidate: Candidate) -> None:\n        \"\"\"An in-place update to overwrite the distribution with a new candidate\"\"\"\n        paths_to_remove = self.get_paths_to_remove(dist)\n        termui.logger.info(\"Overwriting distribution %s\", dist.metadata.get(\"Name\"))\n        installed = self.install(candidate)\n        installed_paths = self.get_paths_to_remove(installed)\n        # Remove the paths that are in the new distribution\n        paths_to_remove.difference_update(installed_paths)\n        try:\n            paths_to_remove.remove()\n            paths_to_remove.commit()\n        except OSError as e:\n            termui.logger.warning(\"Error occurred during overwriting, roll back the changes now.\")\n            paths_to_remove.rollback()\n            raise UninstallError(e) from e\n"
  },
  {
    "path": "src/pdm/installers/synchronizers.py",
    "content": "from __future__ import annotations\n\nimport functools\nimport traceback\nfrom concurrent.futures import Future, ThreadPoolExecutor\nfrom types import SimpleNamespace\nfrom typing import TYPE_CHECKING\n\nfrom pdm import termui\nfrom pdm.exceptions import InstallationError\nfrom pdm.installers.base import BaseSynchronizer\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.reporter import CandidateReporter, InstallationStatus, RichProgressReporter\nfrom pdm.models.requirements import strip_extras\n\nif TYPE_CHECKING:\n    from rich.progress import Progress\n\n    from pdm.compat import Distribution\n\n\nclass Synchronizer(BaseSynchronizer):\n    def install_candidate(self, key: str, progress: Progress) -> Candidate:\n        \"\"\"Install candidate\"\"\"\n        can = self.candidates[key]\n        job = progress.add_task(f\"Installing {can.format()}...\", text=\"\", total=None)\n        can.prepare(self.environment, RichProgressReporter(progress, job))\n        try:\n            self.manager.install(can)\n        except Exception:\n            progress.print(f\"  [error]{termui.Emoji.FAIL}[/] Install {can.format()} failed\")\n            raise\n        else:\n            progress.print(f\"  [success]{termui.Emoji.SUCC}[/] Install {can.format()} successful\")\n        finally:\n            progress.remove_task(job)\n            can.prepare(self.environment, CandidateReporter())\n        return can\n\n    def update_candidate(self, key: str, progress: Progress) -> tuple[Distribution, Candidate]:\n        \"\"\"Update candidate\"\"\"\n        can = self.candidates[key]\n        dist = self.working_set[strip_extras(key)[0]]\n        dist_version = dist.version\n        job = progress.add_task(\n            f\"Updating [req]{key}[/] [warning]{dist_version}[/] -> [warning]{can.version}[/]...\", text=\"\", total=None\n        )\n        can.prepare(self.environment, RichProgressReporter(progress, job))\n        try:\n            self.manager.overwrite(dist, can)\n        except Exception:\n            progress.print(\n                f\"  [error]{termui.Emoji.FAIL}[/] Update [req]{key}[/] \"\n                f\"[warning]{dist_version}[/] \"\n                f\"-> [warning]{can.version}[/] failed\",\n            )\n            raise\n        else:\n            progress.print(\n                f\"  [success]{termui.Emoji.SUCC}[/] Update [req]{key}[/] \"\n                f\"[warning]{dist_version}[/] \"\n                f\"-> [warning]{can.version}[/] successful\",\n            )\n        finally:\n            progress.remove_task(job)\n            can.prepare(self.environment, CandidateReporter())\n\n        return dist, can\n\n    def remove_distribution(self, key: str, progress: Progress) -> Distribution:\n        \"\"\"Remove distributions with given names.\"\"\"\n        dist = self.working_set[key]\n        dist_version = dist.version\n\n        job = progress.add_task(f\"Removing [req]{key}[/] [warning]{dist_version}[/]...\", text=\"\", total=None)\n        try:\n            self.manager.uninstall(dist)\n        except Exception:\n            progress.print(\n                f\"  [error]{termui.Emoji.FAIL}[/] Remove [req]{key}[/] [warning]{dist_version}[/] failed\",\n            )\n            raise\n        else:\n            progress.print(\n                f\"  [success]{termui.Emoji.SUCC}[/] Remove [req]{key}[/] [warning]{dist_version}[/] successful\"\n            )\n        finally:\n            progress.remove_task(job)\n        return dist\n\n    def _show_headline(self, packages: dict[str, list[str]]) -> None:\n        add, update, remove = packages[\"add\"], packages[\"update\"], packages[\"remove\"]\n        if not any((add, update, remove)):\n            self.ui.echo(\"All packages are synced to date, nothing to do.\")\n            return\n        results = [\"[bold]Synchronizing working set with resolved packages[/]:\"]\n        results.extend(\n            [\n                f\"[success]{len(add)}[/] to add,\",\n                f\"[warning]{len(update)}[/] to update,\",\n                f\"[error]{len(remove)}[/] to remove\",\n            ]\n        )\n        self.ui.echo(\" \".join(results) + \"\\n\")\n\n    def _show_summary(self, packages: dict[str, list[str]]) -> None:\n        to_add = [self.candidates[key] for key in packages[\"add\"]]\n        to_update = [(self.working_set[key], self.candidates[key]) for key in packages[\"update\"]]\n        to_remove = [self.working_set[key] for key in packages[\"remove\"]]\n        lines = []\n        if to_add:\n            lines.append(\"[bold]Packages to add[/]:\")\n            for can in to_add:\n                lines.append(f\"  - {can.format()}\")\n        if to_update:\n            lines.append(\"[bold]Packages to update[/]:\")\n            for prev, cur in to_update:\n                lines.append(f\"  - [req]{cur.name}[/] [warning]{prev.version}[/] -> [warning]{cur.version}[/]\")\n        if to_remove:\n            lines.append(\"[bold]Packages to remove[/]:\")\n            for dist in to_remove:\n                lines.append(f\"  - [req]{dist.metadata['Name']}[/] [warning]{dist.version}[/]\")\n        if lines:\n            self.ui.echo(\"\\n\".join(lines))\n        else:\n            self.ui.echo(\"All packages are synced to date, nothing to do.\")\n\n    def _fix_pth_files(self) -> None:\n        \"\"\"Remove the .pdmtmp suffix from the installed packages\"\"\"\n        from pathlib import Path\n\n        lib_paths = self.environment.get_paths()\n        for scheme in [\"purelib\", \"platlib\"]:\n            if not Path(lib_paths[scheme]).exists():\n                continue\n            for path in list(Path(lib_paths[scheme]).iterdir()):\n                if path.suffix == \".pdmtmp\":\n                    target_path = path.with_suffix(\"\")\n                    if target_path.exists():\n                        target_path.unlink()\n                    path.rename(target_path)\n\n    def synchronize(self) -> None:\n        to_add, to_update, to_remove = self.compare_with_working_set()\n        to_do = {\"remove\": to_remove, \"update\": to_update, \"add\": to_add}\n\n        if self.dry_run:\n            self._show_summary(to_do)\n            return\n\n        self._show_headline(to_do)\n        handlers = {\n            \"add\": self.install_candidate,\n            \"update\": self.update_candidate,\n            \"remove\": self.remove_distribution,\n        }\n        sequential_jobs = []\n        parallel_jobs = []\n\n        for kind in to_do:\n            for key in to_do[kind]:\n                if key in self.SEQUENTIAL_PACKAGES or not self.parallel:\n                    sequential_jobs.append((kind, key))\n                elif key in self.candidates and self.candidates[key].req.editable:\n                    # Editable packages are installed sequentially.\n                    sequential_jobs.append((kind, key))\n                else:\n                    parallel_jobs.append((kind, key))\n\n        state = SimpleNamespace(errors=[], parallel_failed=[], sequential_failed=[], jobs=[], mark_failed=False)\n\n        def update_progress(future: Future, kind: str, key: str) -> None:\n            error = future.exception()\n            status.update_spinner(advance=1)  # type: ignore[has-type]\n            if error:\n                exc_info = (type(error), error, error.__traceback__)\n                termui.logger.exception(\"Error occurs %sing %s: \", kind.rstrip(\"e\"), key, exc_info=exc_info)\n                state.parallel_failed.append((kind, key))\n                state.errors.extend([f\"{kind} [success]{key}[/] failed:\\n\", *traceback.format_exception(*exc_info)])\n                if self.fail_fast:\n                    for future in state.jobs:\n                        future.cancel()\n                    state.mark_failed = True\n\n        # get rich progress and live handler to deal with multiple spinners\n        with InstallationStatus(self.ui, \"Synchronizing\") as status:\n            for i in range(self.retry_times + 1):\n                status.update_spinner(completed=0, total=len(sequential_jobs) + len(parallel_jobs))\n                for kind, key in sequential_jobs:\n                    try:\n                        handlers[kind](key, status.progress)\n                    except Exception:\n                        termui.logger.exception(\"Error occurs: \")\n                        state.sequential_failed.append((kind, key))\n                        state.errors.extend([f\"{kind} [success]{key}[/] failed:\\n\", traceback.format_exc()])\n                        if self.fail_fast:\n                            state.mark_failed = True\n                            break\n                    finally:\n                        status.update_spinner(advance=1)\n                if state.mark_failed:\n                    break\n                state.jobs.clear()\n                if parallel_jobs:\n                    with ThreadPoolExecutor() as executor:\n                        for kind, key in parallel_jobs:\n                            future = executor.submit(handlers[kind], key, status.progress)\n                            future.add_done_callback(functools.partial(update_progress, kind=kind, key=key))\n                            state.jobs.append(future)\n                if (\n                    state.mark_failed\n                    or i == self.retry_times\n                    or (not state.sequential_failed and not state.parallel_failed)\n                ):\n                    break\n                sequential_jobs, state.sequential_failed = state.sequential_failed, []\n                parallel_jobs, state.parallel_failed = state.parallel_failed, []\n                state.errors.clear()\n                status.update_spinner(description=f\"Retry failed jobs({i + 2}/{self.retry_times + 1})\")\n\n            try:\n                if state.errors:\n                    if self.ui.verbosity < termui.Verbosity.DETAIL:\n                        status.console.print(\"\\n[error]ERRORS[/]:\")\n                        status.console.print(\"\".join(state.errors), end=\"\")\n                    status.update_spinner(description=f\"[error]{termui.Emoji.FAIL}[/] Some package operations failed.\")\n                    raise InstallationError(\"Some package operations failed.\")\n\n                if self.install_self:\n                    self_key = self.self_key\n                    assert self_key\n                    self.candidates[self_key] = self.self_candidate\n                    word = \"a\" if self.no_editable else \"an editable\"\n                    status.update_spinner(description=f\"Installing the project as {word} package...\")\n                    if self_key in self.working_set:\n                        self.update_candidate(self_key, status.progress)\n                    else:\n                        self.install_candidate(self_key, status.progress)\n\n                status.update_spinner(description=f\"{termui.Emoji.POPPER} All complete!\")\n            finally:\n                # Now we remove the .pdmtmp suffix from the installed packages\n                self._fix_pth_files()\n"
  },
  {
    "path": "src/pdm/installers/uninstallers.py",
    "content": "from __future__ import annotations\n\nimport abc\nimport glob\nimport os\nimport shutil\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom typing import TYPE_CHECKING, Iterable, NewType, TypeVar, cast\n\nfrom pdm import termui\nfrom pdm.exceptions import UninstallError\nfrom pdm.models.cached_package import CachedPackage\nfrom pdm.utils import is_egg_link, is_path_relative_to\n\nif TYPE_CHECKING:\n    from pdm.compat import Distribution\n    from pdm.environments import BaseEnvironment\n\n_T = TypeVar(\"_T\", bound=\"BaseRemovePaths\")\nNormalizedPath = NewType(\"NormalizedPath\", str)\n\n\ndef renames(old: str, new: str) -> None:\n    \"\"\"Like os.renames(), but handles renaming across devices.\"\"\"\n    # Implementation borrowed from os.renames().\n    head, tail = os.path.split(new)\n    if head and tail and not os.path.exists(head):\n        os.makedirs(head)\n\n    shutil.move(old, new)\n\n    head, tail = os.path.split(old)\n    if head and tail:\n        try:\n            os.removedirs(head)\n        except OSError:\n            pass\n\n\ndef compress_for_rename(paths: Iterable[NormalizedPath]) -> set[NormalizedPath]:\n    \"\"\"Returns a set containing the paths that need to be renamed.\n\n    This set may include directories when the original sequence of paths\n    included every file on disk.\n    \"\"\"\n    case_map = {NormalizedPath(os.path.normcase(p)): p for p in paths if os.path.exists(p)}\n    remaining = set(case_map)\n    unchecked = sorted({NormalizedPath(os.path.split(p)[0]) for p in case_map.values()}, key=len)\n    wildcards: set[NormalizedPath] = set()\n\n    def norm_join(*a: str) -> NormalizedPath:\n        return NormalizedPath(os.path.normcase(os.path.join(*a)))\n\n    for root in unchecked:\n        if any(os.path.normcase(root).startswith(w) for w in wildcards):\n            # This directory has already been handled.\n            continue\n\n        all_files: set[NormalizedPath] = set()\n        for dirname, subdirs, files in os.walk(root):\n            all_files.update(norm_join(root, dirname, f) for f in files)\n            for d in subdirs:\n                norm_path = norm_join(root, dirname, d)\n                if os.path.islink(norm_path):\n                    all_files.add(norm_path)\n\n        # If all the files we found are in our remaining set of files to\n        # remove, then remove them from the latter set and add a wildcard\n        # for the directory.\n        if not (all_files - remaining):\n            remaining.difference_update(all_files)\n            wildcards.add(NormalizedPath(root + os.sep))\n\n    collected = set(map(case_map.__getitem__, remaining)) | wildcards\n    shortened: set[NormalizedPath] = set()\n    # Filter out any paths that are sub paths of another path in the path collection.\n    for path in sorted(collected, key=len):\n        if not any(is_path_relative_to(path, p) for p in shortened):\n            shortened.add(path)\n    return shortened\n\n\ndef _script_names(script_name: str, is_gui: bool) -> Iterable[str]:\n    yield script_name\n    if os.name == \"nt\":\n        yield script_name + \".exe\"\n        yield script_name + \".exe.manifest\"\n        if is_gui:\n            yield script_name + \"-script.pyw\"\n        else:\n            yield script_name + \"-script.py\"\n\n\ndef _cache_file_from_source(py_file: NormalizedPath) -> Iterable[NormalizedPath]:\n    py2_cache = py_file[:-3] + \".pyc\"\n    if os.path.isfile(py2_cache):\n        yield NormalizedPath(py2_cache)\n    parent, base = os.path.split(py_file)\n    cache_dir = os.path.join(parent, \"__pycache__\")\n    yield from map(NormalizedPath, glob.glob(os.path.join(cache_dir, base[:-3] + \".*.pyc\")))\n\n\ndef _get_file_root(path: str, base: str) -> str | None:\n    try:\n        rel_path = Path(path).relative_to(base)\n    except ValueError:\n        return None\n    else:\n        root = rel_path.parts[0] if len(rel_path.parts) > 1 else \"\"\n        return os.path.normcase(os.path.join(base, root))\n\n\ndef _get_all_parents(path: NormalizedPath) -> Iterable[NormalizedPath]:\n    while True:\n        yield path\n        parent = NormalizedPath(os.path.split(path)[0])\n        if parent == path:\n            break\n        path = parent\n\n\nclass BaseRemovePaths(abc.ABC):\n    \"\"\"A collection of paths and/or pth entries to remove\"\"\"\n\n    def __init__(self, dist: Distribution, environment: BaseEnvironment) -> None:\n        self.dist = dist\n        self.environment = environment\n        self._paths: set[NormalizedPath] = set()\n        self._pth_entries: set[str] = set()\n        self.refer_to: str | None = None\n\n    def difference_update(self, other: BaseRemovePaths) -> None:\n        self._pth_entries.difference_update(other._pth_entries)\n        for p in other._paths:\n            # if other_p is a file, remove all parent dirs of it\n            self._paths.difference_update(_get_all_parents(p))\n            # other_p is a symlink dir, remove all files under it\n            self._paths.difference_update({p2 for p2 in self._paths if p2.startswith(p + os.sep)})\n\n    @abc.abstractmethod\n    def remove(self) -> None:\n        \"\"\"Remove the files\"\"\"\n\n    @abc.abstractmethod\n    def commit(self) -> None:\n        \"\"\"Commit the removal\"\"\"\n\n    @abc.abstractmethod\n    def rollback(self) -> None:\n        \"\"\"Roll back the removal operations\"\"\"\n\n    @classmethod\n    def from_dist(cls: type[_T], dist: Distribution, environment: BaseEnvironment) -> _T:\n        \"\"\"Create an instance from the distribution\"\"\"\n        scheme = environment.get_paths()\n        instance = cls(dist, environment)\n        meta_location = os.path.normcase(dist._path.absolute())  # type: ignore[attr-defined]\n        dist_location = os.path.dirname(meta_location)\n        if is_egg_link(dist):  # pragma: no cover\n            egg_link_path = cast(\"Path | None\", getattr(dist, \"link_file\", None))\n            dist_name = dist.metadata.get(\"Name\")\n            if not egg_link_path:\n                termui.logger.warning(\n                    \"No egg link is found for editable distribution %s, do nothing.\",\n                    dist_name,\n                )\n            else:\n                with egg_link_path.open(\"rb\") as f:\n                    link_pointer = os.path.normcase(f.readline().decode().strip())\n                if link_pointer != dist_location:\n                    raise UninstallError(\n                        f\"The link pointer in {egg_link_path} doesn't match \"\n                        f\"the location of {dist_name} (at {dist_location}\"\n                    )\n                instance.add_path(str(egg_link_path))\n                instance.add_pth(link_pointer)\n        elif dist.files:\n            for file in dist.files:\n                location = dist.locate_file(file)\n                instance.add_path(str(location))\n                bare_name, ext = os.path.splitext(cast(Path, location))\n                if ext == \".py\":\n                    # .pyc files are added by add_path()\n                    instance.add_path(bare_name + \".pyo\")\n\n            # installed-files.txt isn't recorded in SOURCES.txt for egg-info\n            instance.add_path(os.path.join(meta_location, \"installed-files.txt\"))\n\n        bin_dir = scheme[\"scripts\"]\n\n        if os.path.isdir(os.path.join(meta_location, \"scripts\")):  # pragma: no cover\n            for script in os.listdir(os.path.join(meta_location, \"scripts\")):\n                instance.add_path(os.path.join(bin_dir, script))\n                if os.name == \"nt\":\n                    instance.add_path(os.path.join(bin_dir, script) + \".bat\")\n\n        # find console_scripts\n        _scripts_to_remove: list[str] = []\n        for ep in dist.entry_points:\n            if ep.group == \"console_scripts\":\n                _scripts_to_remove.extend(_script_names(ep.name, False))\n            elif ep.group == \"gui_scripts\":\n                _scripts_to_remove.extend(_script_names(ep.name, True))\n\n        for s in _scripts_to_remove:\n            instance.add_path(os.path.join(bin_dir, s))\n        return instance\n\n    def add_pth(self, line: str) -> None:\n        self._pth_entries.add(line)\n\n    def add_path(self, path: str) -> None:\n        normalized_path = NormalizedPath(os.path.normcase(os.path.expanduser(os.path.abspath(path))))\n        self._paths.add(normalized_path)\n        if path.endswith(\".py\"):\n            self._paths.update(_cache_file_from_source(normalized_path))\n        elif path.replace(\"\\\\\", \"/\").endswith(\".dist-info/REFER_TO\"):\n            with open(path, \"rb\") as f:\n                line = f.readline().decode().strip()\n            if line:\n                self.refer_to = line\n\n\nclass StashedRemovePaths(BaseRemovePaths):\n    \"\"\"Stash the paths to temporarily location and remove them after commit\"\"\"\n\n    PTH_REGISTRY = \"easy-install.pth\"\n\n    def __init__(self, dist: Distribution, environment: BaseEnvironment) -> None:\n        super().__init__(dist, environment)\n        self._pth_file = os.path.join(self.environment.get_paths()[\"purelib\"], self.PTH_REGISTRY)\n        self._saved_pth: bytes | None = None\n        self._stashed: list[tuple[str, str]] = []\n        self._tempdirs: dict[str, TemporaryDirectory] = {}\n\n    def remove(self) -> None:\n        self._remove_pth()\n        self._stash_files()\n\n    def _remove_pth(self) -> None:\n        if not self._pth_entries:\n            return\n        with open(self._pth_file, \"rb\") as f:\n            self._saved_pth = f.read()\n        endline = \"\\r\\n\" if b\"\\r\\n\" in self._saved_pth else \"\\n\"\n        lines = self._saved_pth.decode().splitlines()\n        for item in self._pth_entries:\n            termui.logger.debug(\"Removing pth entry: %s\", item)\n            lines.remove(item)\n        with open(self._pth_file, \"wb\") as f:\n            f.write((endline.join(lines) + endline).encode(\"utf8\"))\n\n    def _stash_files(self) -> None:\n        paths_to_rename = sorted(compress_for_rename(self._paths))\n        prefix = os.path.abspath(self.environment.get_paths()[\"prefix\"])\n\n        for old_path in paths_to_rename:\n            if not os.path.exists(old_path):\n                continue\n            is_dir = os.path.isdir(old_path) and not os.path.islink(old_path)\n            termui.logger.debug(\"Removing %s %s\", \"directory\" if is_dir else \"file\", old_path)\n            if old_path.endswith(\".pyc\"):\n                # Don't stash cache files, remove them directly\n                os.unlink(old_path)\n                continue\n            root = _get_file_root(old_path, prefix)\n            if root is None:\n                termui.logger.debug(\"File path %s is not under packages root %s, skip\", old_path, prefix)\n                continue\n            if root not in self._tempdirs:\n                self._tempdirs[root] = TemporaryDirectory(\"-uninstall\", \"pdm-\")\n            new_root = self._tempdirs[root].name\n            relpath = os.path.relpath(old_path, root)\n            new_path = os.path.join(new_root, relpath)\n            if is_dir and os.path.isdir(new_path):\n                os.rmdir(new_path)\n            renames(old_path, new_path)\n            self._stashed.append((old_path, new_path))\n\n    def commit(self) -> None:\n        for tempdir in self._tempdirs.values():\n            try:\n                tempdir.cleanup()\n            except FileNotFoundError:\n                pass\n        self._tempdirs.clear()\n        self._stashed.clear()\n        self._saved_pth = None\n        if self.refer_to:\n            termui.logger.info(\"Unlink from cached package %s\", self.refer_to)\n            CachedPackage(self.refer_to).remove_referrer(os.path.dirname(self.refer_to))\n            self.refer_to = None\n\n    def rollback(self) -> None:\n        if not self._stashed:\n            termui.logger.error(\"Can't rollback, not uninstalled yet\")\n            return\n        if self._saved_pth is not None:\n            with open(self._pth_file, \"wb\") as f:\n                f.write(self._saved_pth)\n        for old_path, new_path in self._stashed:\n            termui.logger.debug(\"Rollback %s\\n from %s\", old_path, new_path)\n            if os.path.isfile(old_path) or os.path.islink(old_path):\n                os.unlink(old_path)\n            elif os.path.isdir(old_path):\n                shutil.rmtree(old_path)\n            renames(new_path, old_path)\n        self.commit()\n"
  },
  {
    "path": "src/pdm/installers/uv.py",
    "content": "from __future__ import annotations\n\nimport os\nimport subprocess\nfrom typing import Any\n\nfrom pdm._types import HiddenText\nfrom pdm.environments.local import PythonLocalEnvironment\nfrom pdm.exceptions import PdmUsageError, ProjectError\nfrom pdm.installers.base import BaseSynchronizer\nfrom pdm.models.repositories import LockedRepository\nfrom pdm.termui import Verbosity\n\n\nclass UvSynchronizer(BaseSynchronizer):\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        locked_repo = LockedRepository({}, self.environment.project.sources, self.environment)\n        for package in self.packages:\n            if self.no_editable and package.candidate.req.editable:\n                package.candidate.req.editable = False\n            locked_repo.add_package(package)\n        self.locked_repo = locked_repo\n\n    def synchronize(self) -> None:\n        from itertools import chain\n\n        from pdm.formats.uv import uv_file_builder\n\n        if isinstance(self.environment, PythonLocalEnvironment):\n            raise PdmUsageError(\n                \"uv doesn't support PEP 582 local packages, this error occurs because you set use_uv = true.\"\n            )\n\n        if self.dry_run:\n            self.environment.project.core.ui.echo(\"[warning]uv doesn't support dry run mode, skipping installation\")\n            return\n        if self.requirements is not None:\n            requirements = list(self.requirements)\n        else:\n            requirements = list(chain.from_iterable(self.environment.project.all_dependencies.values()))\n        with uv_file_builder(\n            self.environment.project, str(self.environment.python_requires), requirements, self.locked_repo\n        ) as builder:\n            venv_project = self.environment.interpreter.get_venv()\n            if venv_project is None:\n                raise ProjectError(\"uv mode doesn't support non-virtual environments\")\n            builder.build_pyproject_toml()\n            builder.build_uv_lock(include_self=self.install_self)\n            cmd = self._get_sync_command()\n            self.environment.project.core.ui.echo(f\"Running uv sync command: {cmd}\", verbosity=Verbosity.DETAIL)\n            real_cmd = [s.secret if isinstance(s, HiddenText) else s for s in cmd]\n            env = {**os.environ, \"UV_PROJECT_ENVIRONMENT\": str(venv_project.root)}\n            subprocess.run(real_cmd, check=True, cwd=self.environment.project.root, env=env)\n\n    def _get_sync_command(self) -> list[str | HiddenText]:\n        core = self.environment.project.core\n        cmd: list[str | HiddenText] = [\n            *core.uv_cmd,\n            \"sync\",\n            \"--all-extras\",\n            \"--frozen\",\n            \"-p\",\n            str(self.environment.interpreter.executable),\n        ]\n        if core.ui.verbosity > 0:\n            cmd.append(\"--verbose\")\n        if not core.state.enable_cache:\n            cmd.append(\"--no-cache\")\n        if not self.clean and not self.only_keep:\n            cmd.append(\"--inexact\")\n        if self.reinstall:\n            cmd.append(\"--reinstall\")\n        if not self.install_self:\n            cmd.append(\"--no-install-project\")\n        first_index = True\n        for source in self.environment.project.sources:\n            url = source.url_with_credentials\n            if source.type == \"find_links\":\n                cmd.extend([\"--find-links\", url])\n            elif first_index:\n                cmd.extend([\"--index-url\", url])\n                first_index = False\n            else:\n                cmd.extend([\"--extra-index-url\", url])\n        if self.use_install_cache:\n            cmd.extend([\"--link-mode\", self.environment.project.config[\"install.cache_method\"]])\n        return cmd\n\n\nclass QuietUvSynchronizer(UvSynchronizer):\n    def _get_sync_command(self) -> list[str | HiddenText]:\n        cmd = super()._get_sync_command()\n        if \"--verbose\" in cmd:\n            cmd.remove(\"--verbose\")\n        return [*cmd, \"--quiet\"]\n"
  },
  {
    "path": "src/pdm/misc/__init__.py",
    "content": ""
  },
  {
    "path": "src/pdm/misc/sysconfig_patcher.py",
    "content": "# Copyright 2024 Ulrik Sverdrup \"bluss\"\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy of\n# this software and associated documentation files (the \"Software\"), to deal in\n# the Software without restriction, including without limitation the rights to\n# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n# the Software, and to permit persons to whom the Software is furnished to do so,\n# subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n\"\"\"\nPatch sysconfigdata and pkgconfig files in\na python installation from indygreg's python builds.\n\nAdapted from https://github.com/bluss/sysconfigpatcher\n\"\"\"\n\nimport ast\nimport logging\nimport os\nimport re\nimport shutil\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\n_logger = logging.getLogger(__name__)\n\nOLD_PREFIX = \"/install\"\n\nSYSCONFIG_HEADER = \"\"\"\\\n# system configuration generated and used by the sysconfig module\n# install path patched by sysconfigpatcher\n\"\"\"\n\n\n@dataclass\nclass WordReplace:\n    \"\"\"Replace word with another\"\"\"\n\n    word: str\n    to: str\n\n\nDEFAULT_VARIABLE_UPDATES = {\n    \"CC\": WordReplace(\"clang\", \"cc\"),\n    \"CXX\": WordReplace(\"clang++\", \"c++\"),\n    \"BLDSHARED\": WordReplace(\"clang\", \"cc\"),\n    \"LDSHARED\": WordReplace(\"clang\", \"cc\"),\n    \"LDCXXSHARED\": WordReplace(\"clang++\", \"c++\"),\n    \"LINKCC\": WordReplace(\"clang\", \"cc\"),\n    \"AR\": \"ar\",\n}\n\n\ndef read_sysconfig_data_ast(fname):\n    with open(fname) as fn:\n        module = ast.parse(fn.read(), filename=fname)\n    return module\n\n\ndef update_prefix(value: str, real_prefix: str):\n    if value.startswith(OLD_PREFIX):\n        return value.replace(OLD_PREFIX, real_prefix, 1)\n    return value\n\n\ndef sync_file(fn):\n    if hasattr(os, \"fdatasync\"):\n        os.fdatasync(fn)\n    elif hasattr(os, \"fsync\"):\n        os.fsync(fn)\n\n\ndef select_child(ast_obj, type_):\n    return next((elt for elt in ast_obj.body if isinstance(elt, type_)), None)\n\n\ndef patch_sysconfig_ast(obj, real_prefix, variable_updates=None):\n    \"\"\"\n    variable_updates (dict[str, str | WordReplace] | None): Extra variables that should be updated\n    return True if any changes were done\n    \"\"\"\n    did_update = False\n    variable_updates = variable_updates or {}\n\n    # find module body\n    if \"body\" not in obj._fields:\n        raise ValueError\n    # find assignment\n    assignment = select_child(obj, ast.Assign)\n    if assignment is None:\n        raise ValueError\n\n    dict_ast = assignment.value\n\n    if not isinstance(dict_ast, ast.Dict):\n        raise ValueError(f\"Expected Dict, got {dict_ast!r}\")\n\n    real_prefix_str = str(real_prefix)\n    # type check\n    if not all(isinstance(key, ast.Constant) and isinstance(key.value, str) for key in dict_ast.keys):\n        raise ValueError(\"Expected all str keys dict\")\n    if not all(isinstance(value, ast.Constant) and isinstance(value.value, (str, int)) for value in dict_ast.values):\n        raise ValueError(\"Expected all str and int values dict\")\n    # index because we are modifying\n    for key_ast, value_ast in zip(dict_ast.keys, dict_ast.values):\n        if not (isinstance(key_ast, ast.Constant) and isinstance(key_ast.value, str)):\n            raise ValueError(\"Expected all str keys dict\")\n        if not (isinstance(value_ast, ast.Constant) and isinstance(value_ast.value, (str, int))):\n            raise ValueError(\"Expected all str and int values dict\")\n        key = key_ast.value\n        value = value_ast.value\n\n        if not isinstance(value, str):\n            continue\n\n        new_value = None\n\n        if key in variable_updates:\n            updater = variable_updates[key]\n            if isinstance(updater, WordReplace):\n                new_value = re.sub(r\"(^|\\s)\" + re.escape(updater.word) + r\"(?=\\s|$)\", updater.to, value, flags=re.ASCII)\n            else:\n                new_value = updater\n            if value == new_value:\n                new_value = None\n        elif value.startswith(OLD_PREFIX):\n            # some keys have multiple paths like\n            # DESTDIRS=\"/install /install/lib /install/lib/python3.12 (...)\"\n            new_value = \" \".join(update_prefix(part, real_prefix_str) for part in value.split(\" \"))\n\n        if new_value is not None:\n            did_update = True\n            value_ast.value = new_value\n            _logger.debug(\"Updated %r value\\n  from %r\\n  to %r\", key, value, new_value)\n\n    return did_update\n\n\ndef patch_sysconfig(path: Path, real_prefix: Path, dry_run: bool, backup_files: bool, variable_updates=None):\n    \"\"\"\n    return True if did patch or nothing to patch\n    \"\"\"\n    _logger.debug(\"Reading %r\", str(path))\n    obj = read_sysconfig_data_ast(path)\n    did_update = patch_sysconfig_ast(obj, real_prefix, variable_updates)\n\n    if not did_update:\n        _logger.info(\"Nothing to patch in sysconfig\")\n        return True\n\n    new_file = path.with_suffix(\".py.new\")\n    if dry_run:\n        _logger.info(\"Would patch %s\", path)\n    else:\n        if backup_files:\n            backup_file = path.with_suffix(\".py.backup\")\n            shutil.copy(path, backup_file)\n            _logger.debug(\"Wrote %s\", backup_file)\n\n        with open(new_file, \"w\") as fn:\n            fn.write(SYSCONFIG_HEADER)\n            fn.write(ast.unparse(obj))\n            sync_file(fn)\n\n        shutil.move(new_file, path)\n        _logger.info(\"Patched %s\", path)\n    return True\n\n\ndef find_pkgconfigs(path: Path):\n    pkgconfig = path / \"lib/pkgconfig\"\n    if not pkgconfig.exists() or not pkgconfig.is_dir():\n        return\n\n    # is_file and not is_symlink\n    for child in pkgconfig.iterdir():\n        if child.is_file() and not child.is_symlink() and child.suffix == \".pc\":\n            yield child\n\n\ndef write_new_pkgconfig(fname: Path, real_prefix: Path, dest_path: Path):\n    \"\"\"\n    raises ValueError if there is a problem\n    returns True if file was changed\n    \"\"\"\n\n    def replace_func(matchobj):\n        return matchobj.group(1) + str(real_prefix)\n\n    did_update = False\n\n    with open(fname) as fn:\n        with open(dest_path, \"w\") as outfile:\n            for line in fn:\n                new_line = re.sub(\n                    r\"^(\\w+=)(\" + re.escape(OLD_PREFIX) + \")\",\n                    replace_func,\n                    line,\n                    count=1,\n                )\n                if new_line != line:\n                    did_update = True\n                    _logger.debug(\"Updated\\n  from %r\\n  to %r\", line.rstrip(), new_line.rstrip())\n                outfile.write(new_line)\n            sync_file(outfile)\n    return did_update\n\n\ndef patch_pkgconfig(pkgconfig_file, real_prefix, dry_run: bool, backup_files: bool):\n    new_file = pkgconfig_file.with_suffix(\".pc.new\")\n    if dry_run:\n        _logger.info(\"Would patch %s\", pkgconfig_file)\n    else:\n        did_update = write_new_pkgconfig(pkgconfig_file, real_prefix, new_file)\n        if not did_update:\n            os.unlink(new_file)\n            _logger.info(\"Nothing to patch for %s\", pkgconfig_file)\n        else:\n            if backup_files:\n                backup_file = pkgconfig_file.with_suffix(\".pc.backup\")\n                shutil.copy(pkgconfig_file, backup_file)\n                _logger.debug(\"Wrote %s\", backup_file)\n            shutil.move(new_file, pkgconfig_file)\n            _logger.info(\"Patched %s\", pkgconfig_file)\n\n\ndef find_libdir(real_prefix: Path):\n    # probe python for its libdir and path to the file\n    libdir = real_prefix / \"lib\"\n    # find python3.xy in libdir\n\n    if libdir.is_dir():\n        py_children = [child for child in libdir.iterdir() if child.is_dir() and child.name.startswith(\"python3\")]\n        if len(py_children) == 1:\n            return py_children[0]\n        if len(py_children) > 1:\n            _logger.info(\"Found lib directories: %r\", py_children)\n    _logger.error(\"No lib/python3.x directory found\")\n    return None\n\n\ndef find_sysconfigdata(real_prefix: Path):\n    # probe python for its libdir and path to the file\n    sysconfigdata_prefix = \"_sysconfigdata_\"\n    libdir = find_libdir(real_prefix)\n    if libdir is None:\n        return None\n    for child in libdir.iterdir():\n        if (\n            child.is_file()\n            and not child.is_symlink()\n            and child.name.startswith(sysconfigdata_prefix)\n            and child.suffix == \".py\"\n        ):\n            return child\n    return None\n\n\ndef patch(real_prefix: Path) -> None:\n    if os.name == \"nt\":\n        # sysconfig patching not needed on Windows\n        return\n    sysconfig_path = find_sysconfigdata(real_prefix)\n    if sysconfig_path is None:\n        _logger.error(\"No sysconfigdata file found\")\n    else:\n        patch_sysconfig(\n            sysconfig_path, real_prefix, dry_run=False, backup_files=False, variable_updates=DEFAULT_VARIABLE_UPDATES\n        )\n\n    pkgconfig_files = list(find_pkgconfigs(real_prefix))\n    if not pkgconfig_files:\n        _logger.info(\"No pkgconfig files found\")\n    for pc_file in pkgconfig_files:\n        patch_pkgconfig(pc_file, real_prefix, dry_run=False, backup_files=False)\n"
  },
  {
    "path": "src/pdm/models/__init__.py",
    "content": ""
  },
  {
    "path": "src/pdm/models/auth.py",
    "content": "from __future__ import annotations\n\nimport functools\nimport urllib.parse\n\nfrom unearth.auth import MaybeAuth, MultiDomainBasicAuth, get_keyring_provider\nfrom unearth.utils import commonprefix, split_auth_from_url\n\nfrom pdm._types import RepositoryConfig\nfrom pdm.exceptions import PdmException\nfrom pdm.termui import UI, Verbosity, is_interactive\n\n\nclass PdmBasicAuth(MultiDomainBasicAuth):\n    \"\"\"A custom auth class that differs from Pip's implementation in the\n    following ways:\n\n        - It shows an error message when credentials are not provided or correct.\n    \"\"\"\n\n    def __init__(self, ui: UI, sources: list[RepositoryConfig]) -> None:\n        super().__init__(prompting=is_interactive())\n        self.sources = sources\n        self.ui = ui\n        self._selected_source: RepositoryConfig | None = None\n\n    def _get_new_credentials(\n        self, original_url: str, *, allow_netrc: bool = True, allow_keyring: bool = False\n    ) -> tuple[str | None, str | None]:\n        user, password = super()._get_new_credentials(\n            original_url, allow_netrc=allow_netrc, allow_keyring=allow_keyring\n        )\n        if (user is None or password is None) and allow_keyring and self._selected_source:\n            self._selected_source.populate_keyring_auth()\n            user = user or self._selected_source.username\n            password = password or self._selected_source.password\n        self._selected_source = None\n        return user, password\n\n    def _get_auth_from_index_url(self, url: str) -> tuple[MaybeAuth, str | None]:\n        if not self.sources:\n            return None, None\n\n        target = urllib.parse.urlsplit(url.rstrip(\"/\") + \"/\")\n        candidates: list[tuple[MaybeAuth, str, urllib.parse.SplitResult, RepositoryConfig]] = []\n        for source in self.sources:\n            assert source.url\n            index = source.url.rstrip(\"/\") + \"/\"\n            auth, url_no_auth = split_auth_from_url(index)\n            parsed = urllib.parse.urlsplit(url_no_auth)\n            if source.username:\n                auth = (source.username, source.password)\n            if parsed == target:\n                return auth, index\n            if parsed.netloc == target.netloc:\n                candidates.append((auth, index, parsed, source))\n\n        if not candidates:\n            return None, None\n        auth, index, _, source = max(candidates, key=lambda x: commonprefix(x[2].path, target.path).rfind(\"/\"))\n        self._selected_source = source\n        return auth, index\n\n    def _prompt_for_password(self, netloc: str, username: str | None = None) -> tuple[str | None, str | None, bool]:\n        if self.ui.verbosity < Verbosity.DETAIL:\n            raise PdmException(\n                f\"The credentials for {netloc} are not provided. To give them via interactive shell, \"\n                \"please rerun the command with `-v` option.\"\n            )\n        return super()._prompt_for_password(netloc, username)\n\n    def _should_save_password_to_keyring(self) -> bool:\n        if get_keyring_provider() is None:\n            self.ui.info(\n                \"The provided credentials will not be saved into the keyring.\\n\"\n                \"You can enable this by installing keyring:\\n\"\n                \"    [success]pdm self add keyring[/]\"\n            )\n        return super()._should_save_password_to_keyring()\n\n\nclass Keyring:\n    def __init__(self) -> None:\n        self.provider = get_keyring_provider()\n        self.enabled = self.provider is not None\n\n    @functools.lru_cache(maxsize=128)\n    def get_auth_info(self, url: str, username: str | None) -> tuple[str, str] | None:\n        \"\"\"Return the password for the given url and username.\n        The username can be None.\n        \"\"\"\n        if self.provider is None or not self.enabled:\n            return None\n        try:\n            return self.provider.get_auth_info(url, username)\n        except Exception:\n            self.enabled = False\n            return None\n\n    def save_auth_info(self, url: str, username: str, password: str) -> bool:\n        \"\"\"Set the password for the given url and username.\n        Returns whether the operation is successful.\n        \"\"\"\n        if self.provider is None or not self.enabled:\n            return False\n        try:\n            self.provider.save_auth_info(url, username, password)\n            return True\n        except Exception:\n            self.enabled = False\n            return False\n\n    def delete_auth_info(self, url: str, username: str) -> bool:\n        \"\"\"Delete the password for the given url and username.\n        Returns whether the operation is successful.\n        \"\"\"\n        if self.provider is None or not self.enabled:\n            return False\n        try:\n            self.provider.delete_auth_info(url, username)\n            return True\n        except Exception:\n            self.enabled = False\n            return False\n\n\nkeyring = Keyring()\n"
  },
  {
    "path": "src/pdm/models/backends.py",
    "content": "from __future__ import annotations\n\nimport abc\nimport os\nimport urllib.parse\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nfrom pdm.utils import expand_env_vars\n\nif TYPE_CHECKING:\n    from typing import TypedDict\n\n    BuildSystem = TypedDict(\"BuildSystem\", {\"requires\": list[str], \"build-backend\": str})\n\n\nclass BuildBackend(metaclass=abc.ABCMeta):\n    \"\"\"A build backend that does not support dynamic values in dependencies\"\"\"\n\n    def __init__(self, root: Path) -> None:\n        self.root = root\n\n    def expand_line(self, line: str, expand_env: bool = True) -> str:\n        return line\n\n    def relative_path_to_url(self, path: str) -> str:\n        return self.root.joinpath(path).as_uri()\n\n    @classmethod\n    @abc.abstractmethod\n    def build_system(cls) -> BuildSystem:\n        pass\n\n\nclass FlitBackend(BuildBackend):\n    @classmethod\n    def build_system(cls) -> BuildSystem:\n        return {\n            \"requires\": [\"flit_core>=3.2,<4\"],\n            \"build-backend\": \"flit_core.buildapi\",\n        }\n\n\nclass SetuptoolsBackend(BuildBackend):\n    @classmethod\n    def build_system(cls) -> BuildSystem:\n        return {\n            \"requires\": [\"setuptools>=61\"],\n            \"build-backend\": \"setuptools.build_meta\",\n        }\n\n\nclass PDMBackend(BuildBackend):\n    def expand_line(self, req: str, expand_env: bool = True) -> str:\n        line = req.replace(\"file:///${PROJECT_ROOT}\", self.root.as_uri())\n        if expand_env:\n            line = expand_env_vars(line)\n        return line\n\n    def relative_path_to_url(self, path: str) -> str:\n        if os.path.isabs(path):\n            return Path(path).as_uri()\n        return f\"file:///${{PROJECT_ROOT}}/{urllib.parse.quote(path)}\"\n\n    @classmethod\n    def build_system(cls) -> BuildSystem:\n        return {\n            \"requires\": [\"pdm-backend\"],\n            \"build-backend\": \"pdm.backend\",\n        }\n\n\n# Context formatting helpers for hatch\nclass PathContext:\n    def __init__(self, path: Path) -> None:\n        self.__path = path\n\n    def __format__(self, __format_spec: str) -> str:\n        if not __format_spec:\n            return self.__path.as_posix()\n        elif __format_spec == \"uri\":\n            return self.__path.as_uri()\n        elif __format_spec == \"real\":\n            return self.__path.resolve().as_posix()\n        raise ValueError(f\"Unknown format specifier: {__format_spec}\")\n\n\nclass EnvContext:\n    def __init__(self, expand: bool = True) -> None:\n        self.expand = expand\n\n    def __format__(self, __format_spec: str) -> str:\n        name, sep, default = __format_spec.partition(\":\")\n        if not self.expand:\n            return f\"${{{name}}}\"\n        if name in os.environ:\n            return os.environ[name]\n        if not sep:\n            raise ValueError(f\"Nonexistent environment variable must set a default: {name}\")\n        return default\n\n\nclass HatchBackend(BuildBackend):\n    def expand_line(self, line: str, expand_env: bool = True) -> str:\n        return line.format(\n            env=EnvContext(expand=expand_env),\n            root=PathContext(self.root),\n            home=PathContext(Path.home()),\n        )\n\n    def relative_path_to_url(self, path: str) -> str:\n        if os.path.isabs(path):\n            return Path(path).as_uri()\n        return f\"{{root:uri}}/{urllib.parse.quote(path)}\"\n\n    @classmethod\n    def build_system(cls) -> BuildSystem:\n        return {\n            \"requires\": [\"hatchling\"],\n            \"build-backend\": \"hatchling.build\",\n        }\n\n\n_BACKENDS: dict[str, type[BuildBackend]] = {\n    \"pdm-backend\": PDMBackend,\n    \"setuptools\": SetuptoolsBackend,\n    \"flit-core\": FlitBackend,\n    \"hatchling\": HatchBackend,\n}\n# Fallback to the first backend\nDEFAULT_BACKEND = next(iter(_BACKENDS.values()))\n\n\ndef get_backend(name: str) -> type[BuildBackend]:\n    \"\"\"Get the build backend class by name\"\"\"\n    return _BACKENDS[name]\n\n\ndef get_backend_by_spec(spec: dict) -> type[BuildBackend]:\n    \"\"\"Get the build backend class by specification.\n    The parameter passed in is the 'build-system' section in pyproject.toml.\n    \"\"\"\n    if \"build-backend\" not in spec:\n        return DEFAULT_BACKEND\n    for backend_cls in _BACKENDS.values():\n        if backend_cls.build_system()[\"build-backend\"] == spec[\"build-backend\"]:\n            return backend_cls\n    return DEFAULT_BACKEND\n\n\ndef get_relative_path(url: str) -> str | None:\n    if url.startswith(\"file:///${PROJECT_ROOT}\"):\n        return urllib.parse.unquote(url[len(\"file:///${PROJECT_ROOT}/\") :])\n    if url.startswith(\"{root:uri}\"):\n        return urllib.parse.unquote(url[len(\"{root:uri}/\") :])\n    return None\n"
  },
  {
    "path": "src/pdm/models/cached_package.py",
    "content": "from __future__ import annotations\n\nimport os\nimport shutil\nfrom functools import cached_property\nfrom pathlib import Path\nfrom typing import Any, ClassVar, ContextManager\n\nfrom pdm.termui import logger\n\n\nclass CachedPackage:\n    \"\"\"A package cached in the central package store.\n    The directory name is similar to wheel's filename:\n\n        $PACKAGE_ROOT/<checksum[:2]>/<dist_name>-<version>-<impl>-<abi>-<plat>/\n\n    The checksum is stored in a file named `.checksum` under the directory.\n\n    Under the directory there could be a text file named `.referrers`.\n    Each line of the file is a distribution path that refers to this package.\n    *Only wheel installations will be cached*\n    \"\"\"\n\n    cache_files: ClassVar[tuple[str, ...]] = (\".lock\", \".checksum\", \".referrers\")\n    \"\"\"List of files storing cache metadata and not being part of the package\"\"\"\n\n    def __init__(self, path: str | Path, original_wheel: Path | None = None) -> None:\n        self.path = Path(os.path.normcase(os.path.expanduser(path))).resolve()\n        self.original_wheel = original_wheel\n        self._referrers: set[str] | None = None\n\n    def lock(self) -> ContextManager[Any]:\n        import filelock\n\n        return filelock.FileLock(self.path / \".lock\")\n\n    @cached_property\n    def checksum(self) -> str:\n        \"\"\"The checksum of the path\"\"\"\n        return self.path.joinpath(\".checksum\").read_text().strip()\n\n    @cached_property\n    def dist_info(self) -> Path:\n        \"\"\"The dist-info directory of the wheel\"\"\"\n        from installer.exceptions import InvalidWheelSource\n\n        try:\n            return next(self.path.glob(\"*.dist-info\"))\n        except StopIteration:\n            raise InvalidWheelSource(f\"The wheel doesn't contain metadata {self.path!r}\") from None\n\n    @property\n    def referrers(self) -> set[str]:\n        \"\"\"A set of entries in referrers file\"\"\"\n        if self._referrers is None:\n            filepath = self.path / \".referrers\"\n            if not filepath.is_file():\n                return set()\n            self._referrers = {\n                line.strip()\n                for line in filepath.read_text(\"utf8\").splitlines()\n                if line.strip() and os.path.exists(line.strip())\n            }\n        return self._referrers\n\n    def add_referrer(self, path: str) -> None:\n        \"\"\"Add a new referrer\"\"\"\n        path = os.path.normcase(os.path.expanduser(os.path.abspath(path)))\n        referrers = self.referrers | {path}\n        (self.path / \".referrers\").write_text(\"\\n\".join(sorted(referrers)) + \"\\n\", \"utf8\")\n        self._referrers = None\n\n    def remove_referrer(self, path: str) -> None:\n        \"\"\"Remove a referrer\"\"\"\n        path = os.path.normcase(os.path.expanduser(os.path.abspath(path)))\n        referrers = self.referrers - {path}\n        (self.path / \".referrers\").write_text(\"\\n\".join(referrers) + \"\\n\", \"utf8\")\n        self._referrers = None\n\n    def cleanup(self) -> None:\n        logger.info(\"Clean up cached package %s\", self.path)\n        shutil.rmtree(self.path)\n"
  },
  {
    "path": "src/pdm/models/caches.py",
    "content": "from __future__ import annotations\n\nimport contextlib\nimport hashlib\nimport json\nimport os\nimport stat\nfrom functools import lru_cache\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Generic, Iterable, TypeVar\n\nfrom packaging.utils import canonicalize_name, parse_wheel_filename\n\nfrom pdm._types import CandidateInfo\nfrom pdm.exceptions import PdmException\nfrom pdm.models.cached_package import CachedPackage\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.markers import EnvSpec\nfrom pdm.termui import logger\nfrom pdm.utils import atomic_open_for_write, create_tracked_tempdir\n\nif TYPE_CHECKING:\n    from httpx import Client\n    from unearth import Link\n\n\nKT = TypeVar(\"KT\")\nVT = TypeVar(\"VT\")\n\n\nclass JSONFileCache(Generic[KT, VT]):\n    \"\"\"A file cache that stores key-value pairs in a json file.\"\"\"\n\n    def __init__(self, cache_file: Path | str) -> None:\n        self.cache_file = Path(cache_file)\n        self._cache: dict[str, VT] = {}\n        self._read_cache()\n\n    def _read_cache(self) -> None:\n        if not self.cache_file.exists():\n            self._cache = {}\n            return\n        with self.cache_file.open() as fp:\n            try:\n                self._cache = json.load(fp)\n            except json.JSONDecodeError:\n                return\n\n    def _write_cache(self) -> None:\n        with self.cache_file.open(\"w\") as fp:\n            json.dump(self._cache, fp)\n\n    def __contains__(self, obj: KT) -> bool:\n        return self._get_key(obj) in self._cache\n\n    @classmethod\n    def _get_key(cls, obj: KT) -> str:\n        return str(obj)\n\n    def get(self, obj: KT) -> VT:\n        key = self._get_key(obj)\n        return self._cache[key]\n\n    def set(self, obj: KT, value: VT) -> None:\n        key = self._get_key(obj)\n        self._cache[key] = value\n        self._write_cache()\n\n\nclass CandidateInfoCache(JSONFileCache[Candidate, CandidateInfo]):\n    \"\"\"A cache manager that stores the\n    candidate -> (dependencies, requires_python, summary) mapping.\n    \"\"\"\n\n    @staticmethod\n    def get_url_part(link: Link) -> str:\n        import base64\n\n        from pdm.utils import url_without_fragments\n\n        url = url_without_fragments(link.split_auth()[1])\n        return base64.urlsafe_b64encode(url.encode()).decode()\n\n    @classmethod\n    def _get_key(cls, obj: Candidate) -> str:\n        # Name and version are set when dependencies are resolved,\n        # so use them for cache key. Local directories won't be cached.\n        if not obj.name or not obj.version:\n            raise KeyError(\"The package is missing a name or version\")\n        extras = \"[{}]\".format(\",\".join(sorted(obj.req.extras))) if obj.req.extras else \"\"\n        version = obj.version\n        if obj.link is not None and not obj.req.is_named:\n            version = cls.get_url_part(obj.link)\n        return f\"{obj.name}{extras}-{version}\"\n\n\nclass HashCache:\n    \"\"\"Caches hashes of PyPI artifacts so we do not need to re-download them.\n\n    Hashes are only cached when the URL appears to contain a hash in it and the\n    cache key includes the hash value returned from the server). This ought to\n    avoid issues where the location on the server changes.\n    \"\"\"\n\n    FAVORITE_HASH = \"sha256\"\n    STRONG_HASHES = (\"sha256\", \"sha384\", \"sha512\")\n\n    def __init__(self, directory: Path | str) -> None:\n        self.directory = Path(directory)\n\n    def _read_from_link(self, link: Link, session: Client) -> Iterable[bytes]:\n        if link.is_file:\n            with open(link.file_path, \"rb\") as f:\n                yield from f\n        else:\n            import httpx\n\n            with session.stream(\"GET\", link.normalized) as resp:\n                try:\n                    resp.raise_for_status()\n                except httpx.HTTPStatusError as e:\n                    raise PdmException(f\"Failed to read from {link.redacted}: {e}\") from e\n                yield from resp.iter_bytes(chunk_size=8192)\n\n    def _get_file_hash(self, link: Link, session: Client) -> str:\n        h = hashlib.new(self.FAVORITE_HASH)\n        logger.debug(\"Downloading link %s for calculating hash\", link.redacted)\n        for chunk in self._read_from_link(link, session):\n            h.update(chunk)\n        return \":\".join([h.name, h.hexdigest()])\n\n    def _should_cache(self, link: Link) -> bool:\n        # For now, we only disable caching for local files.\n        # We may add more when we know better about it.\n        return not link.is_file\n\n    def get_hash(self, link: Link, session: Client) -> str:\n        # If there is no link hash (i.e., md5, sha256, etc.), we don't want\n        # to store it.\n        hash_value = self.get(link.url_without_fragment)\n        if not hash_value:\n            if link.hashes and link.hashes.keys() & self.STRONG_HASHES:\n                logger.debug(\"Using hash in link for %s\", link.redacted)\n                hash_name = next(k for k in self.STRONG_HASHES if k in link.hashes)\n                hash_value = f\"{hash_name}:{link.hashes[hash_name]}\"\n            elif link.hash and link.hash_name in self.STRONG_HASHES:\n                logger.debug(\"Using hash in link for %s\", link.redacted)\n                hash_value = f\"{link.hash_name}:{link.hash}\"\n            else:\n                hash_value = self._get_file_hash(link, session)\n            if self._should_cache(link):\n                self.set(link.url_without_fragment, hash_value)\n        return hash_value\n\n    def _get_path_for_key(self, key: str) -> Path:\n        hashed = hashlib.sha224(key.encode(\"utf-8\")).hexdigest()\n        parts = (hashed[:2], hashed[2:4], hashed[4:6], hashed[6:8], hashed[8:])\n        return self.directory.joinpath(*parts)\n\n    def get(self, url: str) -> str | None:\n        path = self._get_path_for_key(url)\n        with contextlib.suppress(OSError, UnicodeError):\n            return path.read_text(\"utf-8\").strip()\n        return None\n\n    def set(self, url: str, hash: str) -> None:\n        path = self._get_path_for_key(url)\n        with contextlib.suppress(OSError, UnicodeError):\n            path.parent.mkdir(parents=True, exist_ok=True)\n            with atomic_open_for_write(path, encoding=\"utf-8\") as fp:\n                fp.write(hash)\n\n\nclass EmptyCandidateInfoCache(CandidateInfoCache):\n    def get(self, obj: Candidate) -> CandidateInfo:\n        raise KeyError\n\n    def set(self, obj: Candidate, value: CandidateInfo) -> None:\n        pass\n\n\nclass EmptyHashCache(HashCache):\n    def get(self, url: str) -> str | None:\n        return None\n\n    def set(self, url: str, hash: str) -> None:\n        pass\n\n\nclass WheelCache:\n    \"\"\"Caches wheels so we do not need to rebuild them.\n\n    Wheels are only cached when the URL contains egg-info or is a VCS repository\n    with an *immutable* revision. There might be more than one wheels built for\n    one sdist, the one with most preferred tag will be returned.\n    \"\"\"\n\n    def __init__(self, directory: Path | str) -> None:\n        self.directory = Path(directory)\n        self.ephemeral_directory = Path(create_tracked_tempdir(prefix=\"pdm-wheel-cache-\"))\n\n    def _get_candidates(self, path: Path) -> Iterable[Path]:\n        if not path.exists():\n            return\n        for candidate in path.iterdir():\n            if candidate.name.endswith(\".whl\"):\n                yield candidate\n\n    def _get_path_parts(self, link: Link, env_spec: EnvSpec) -> tuple[str, ...]:\n        hash_key = {\n            \"url\": link.url_without_fragment,\n            # target env participates in the hash key to handle the some cases\n            # where the sdist produces different wheels on different Pythons, and\n            # the differences are not encoded in compatibility tags.\n            \"env_spec\": env_spec.as_dict(),\n        }\n        if link.subdirectory:\n            hash_key[\"subdirectory\"] = link.subdirectory\n        if link.hash and link.hash_name:\n            hash_key[link.hash_name] = link.hash\n        hashed = hashlib.sha224(\n            json.dumps(hash_key, sort_keys=True, separators=(\",\", \":\"), ensure_ascii=True).encode(\"utf-8\")\n        ).hexdigest()\n        return (hashed[:2], hashed[2:4], hashed[4:6], hashed[6:])\n\n    def get_path_for_link(self, link: Link, env_spec: EnvSpec) -> Path:\n        parts = self._get_path_parts(link, env_spec)\n        return self.directory.joinpath(*parts)\n\n    def get_ephemeral_path_for_link(self, link: Link, env_spec: EnvSpec) -> Path:\n        parts = self._get_path_parts(link, env_spec)\n        return self.ephemeral_directory.joinpath(*parts)\n\n    def get(self, link: Link, project_name: str | None, env_spec: EnvSpec) -> Path | None:\n        if not project_name:\n            return None\n        canonical_name = canonicalize_name(project_name)\n\n        candidate = self._get_from_path(self.get_path_for_link(link, env_spec), canonical_name, env_spec)\n        if candidate is not None:\n            return candidate\n        return self._get_from_path(self.get_ephemeral_path_for_link(link, env_spec), canonical_name, env_spec)\n\n    def _get_from_path(self, path: Path, canonical_name: str, env_spec: EnvSpec) -> Path | None:\n        max_compatible_candidate: tuple[tuple[int, ...], Path | None] = ((-1, -1, -1, -1), None)\n        for candidate in self._get_candidates(path):\n            try:\n                name, *_ = parse_wheel_filename(candidate.name)\n            except ValueError:\n                logger.debug(\"Ignoring invalid cached wheel %s\", candidate.name)\n                continue\n            if canonical_name != canonicalize_name(name):\n                logger.debug(\n                    \"Ignoring cached wheel %s with invalid project name %s, expected: %s\",\n                    candidate.name,\n                    name,\n                    canonical_name,\n                )\n                continue\n            compat = env_spec.wheel_compatibility(candidate.name)\n            if compat is None:\n                continue\n            if compat > max_compatible_candidate[0]:\n                max_compatible_candidate = (compat, candidate)\n        return max_compatible_candidate[1]\n\n\n@lru_cache(maxsize=None)\ndef get_wheel_cache(directory: Path | str) -> WheelCache:\n    return WheelCache(directory)\n\n\nclass PackageCache:\n    def __init__(self, root: Path) -> None:\n        self.root = root\n\n    def cache_wheel(self, wheel: Path) -> CachedPackage:\n        \"\"\"Create a CachedPackage instance from a wheel file\"\"\"\n        import zipfile\n\n        from installer.utils import make_file_executable\n\n        dest = self.root.joinpath(f\"{wheel.name}.cache\")\n        pkg = CachedPackage(dest, original_wheel=wheel)\n        if dest.exists():\n            return pkg\n        dest.mkdir(parents=True, exist_ok=True)\n        with pkg.lock():\n            logger.info(\"Unpacking wheel into cached location %s\", dest)\n            with zipfile.ZipFile(wheel) as zf:\n                try:\n                    for item in zf.infolist():\n                        target_path = zf.extract(item, dest)\n                        mode = item.external_attr >> 16\n                        is_executable = bool(mode and stat.S_ISREG(mode) and mode & 0o111)\n                        if is_executable:\n                            make_file_executable(target_path)\n                except Exception:  # pragma: no cover\n                    pkg.cleanup()  # cleanup on any error\n                    raise\n        return pkg\n\n    def iter_packages(self) -> Iterable[CachedPackage]:\n        for path in self.root.rglob(\"*.whl.cache\"):\n            p = CachedPackage(path)\n            with p.lock():  # ensure the package is not being created\n                pass\n            yield p\n\n    def cleanup(self) -> int:\n        \"\"\"Remove unused cached packages\"\"\"\n        count = 0\n        for pkg in self.iter_packages():\n            if not any(os.path.exists(fn) for fn in pkg.referrers):\n                pkg.cleanup()\n                count += 1\n        return count\n"
  },
  {
    "path": "src/pdm/models/candidates.py",
    "content": "from __future__ import annotations\n\nimport dataclasses\nimport hashlib\nimport os\nimport re\nimport warnings\nfrom functools import cached_property\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom typing import TYPE_CHECKING, Any, ClassVar, cast, no_type_check\nfrom zipfile import ZipFile\n\nfrom packaging.version import InvalidVersion\n\nfrom pdm import termui\nfrom pdm.builders import EditableBuilder, WheelBuilder\nfrom pdm.compat import importlib_metadata as im\nfrom pdm.exceptions import BuildError, CandidateNotFound, InvalidPyVersion, PDMWarning, RequirementError\nfrom pdm.models.backends import get_backend, get_backend_by_spec\nfrom pdm.models.reporter import CandidateReporter\nfrom pdm.models.requirements import (\n    FileRequirement,\n    Requirement,\n    VcsRequirement,\n    _egg_info_re,\n    filter_requirements_with_extras,\n)\nfrom pdm.models.setup import Setup\nfrom pdm.models.specifiers import PySpecSet\nfrom pdm.utils import (\n    comparable_version,\n    convert_hashes,\n    filtered_sources,\n    get_rev_from_url,\n    normalize_name,\n    url_without_fragments,\n)\n\nif TYPE_CHECKING:\n    from importlib.metadata import _SimplePath\n\n    from unearth import Link, Package, PackageFinder\n\n    from pdm._types import FileHash\n    from pdm.environments import BaseEnvironment\n\n\ndef _dist_info_files(whl_zip: ZipFile) -> list[str]:\n    \"\"\"Identify the .dist-info folder inside a wheel ZipFile.\"\"\"\n    res = []\n    for path in whl_zip.namelist():\n        m = re.match(r\"[^/\\\\]+-[^/\\\\]+\\.dist-info/\", path)\n        if m:\n            res.append(path)\n    if res:\n        return res\n    raise Exception(\"No .dist-info folder found in wheel\")\n\n\ndef _get_wheel_metadata_from_wheel(whl_file: Path, metadata_directory: str) -> str:\n    \"\"\"Extract the metadata from a wheel.\n    Fallback for when the build backend does not\n    define the 'get_wheel_metadata' hook.\n    \"\"\"\n    with ZipFile(whl_file) as zipf:\n        dist_info = _dist_info_files(zipf)\n        zipf.extractall(path=metadata_directory, members=dist_info)\n    return os.path.join(metadata_directory, dist_info[0].split(\"/\")[0])\n\n\ndef _filter_none(data: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"Return a new dict without None values\"\"\"\n    return {k: v for k, v in data.items() if v is not None}\n\n\ndef _find_best_match_link(finder: PackageFinder, req: Requirement, files: list[FileHash]) -> Link | None:\n    \"\"\"Get the best matching link for a requirement\"\"\"\n\n    # This function is called when a lock file candidate is given or incompatible wheel\n    # In this case, the requirement must be pinned, so no need to pass allow_prereleases\n    # If links are not empty, find the best match from the links, otherwise find from\n    # the package sources.\n    from unearth import Link\n\n    links = [Link(f[\"url\"]) for f in files if \"url\" in f]\n    hashes = convert_hashes(files)\n\n    if not links:\n        best = finder.find_best_match(req.as_line(), hashes=hashes).best\n    else:\n        # this branch won't be executed twice if ignore_compatibility is True\n        evaluator = finder.build_evaluator(req.name)\n        packages = finder._evaluate_links(links, evaluator)\n        best = max(packages, key=finder._sort_key, default=None)\n    return best.link if best is not None else None\n\n\nclass MetadataDistribution(im.Distribution):\n    \"\"\"A wrapper around a single METADATA file to provide the Distribution interface\"\"\"\n\n    def __init__(self, text: str) -> None:\n        self.text = text\n\n    def locate_file(self, path: str | os.PathLike[str]) -> _SimplePath:\n        return Path()\n\n    def read_text(self, filename: str) -> str | None:\n        if filename != \"\":\n            return None\n        return self.text\n\n\nclass Candidate:\n    \"\"\"A concrete candidate that can be downloaded and installed.\n    A candidate comes from the PyPI index of a package, or from the requirement itself\n    (for file or VCS requirements). Each candidate has a name, version and several\n    dependencies together with package metadata.\n    \"\"\"\n\n    __slots__ = (\n        \"_preferred\",\n        \"_prepared\",\n        \"_requires_python\",\n        \"_revision\",\n        \"hashes\",\n        \"installed\",\n        \"link\",\n        \"name\",\n        \"req\",\n        \"requested\",\n        \"summary\",\n        \"version\",\n    )\n\n    def __init__(\n        self,\n        req: Requirement,\n        name: str | None = None,\n        version: str | None = None,\n        link: Link | None = None,\n        installed: im.Distribution | None = None,\n    ):\n        \"\"\"\n        :param req: the requirement that produces this candidate.\n        :param name: the name of the candidate.\n        :param version: the version of the candidate.\n        :param link: the file link of the candidate.\n        \"\"\"\n        self.req = req\n        self.name = name or self.req.project_name\n        self.version = version\n        if link is None and not req.is_named:\n            link = cast(\"Link\", req.as_file_link())  # type: ignore[attr-defined]\n        self.link = link\n        self.summary = \"\"\n        self.hashes: list[FileHash] = []\n        self.requested = False\n        self.installed: im.Distribution | None = installed\n\n        self._requires_python: str | None = None\n        self._prepared: PreparedCandidate | None = None\n        self._revision = getattr(req, \"revision\", None)\n\n    def identify(self) -> str:\n        return self.req.identify()\n\n    def copy_with(self, requirement: Requirement) -> Candidate:\n        can = Candidate(requirement, name=self.name, version=self.version, link=self.link, installed=self.installed)\n        can.summary = self.summary\n        can.hashes = self.hashes\n        can._requires_python = self._requires_python\n        can._prepared = self._prepared\n        can._revision = self._revision\n        if can._prepared:\n            can._prepared.req = requirement\n        return can\n\n    @property\n    def dep_key(self) -> tuple[str, str | None]:\n        \"\"\"Key for retrieving and storing dependencies from the provider.\n\n        Return a tuple of (name, version). For URL candidates, the version is None but\n        there will be only one for the same name so it is also unique.\n        \"\"\"\n        return (self.identify(), self.version)\n\n    @property\n    def prepared(self) -> PreparedCandidate | None:\n        return self._prepared\n\n    def __eq__(self, other: Any) -> bool:\n        if not isinstance(other, Candidate):\n            return False\n        if self.req.is_named:\n            return self.name == other.name and self.version == other.version\n        return self.name == other.name and self.link == other.link\n\n    def get_revision(self) -> str:\n        if not self.req.is_vcs:\n            raise AttributeError(\"Non-VCS candidate doesn't have revision attribute\")\n        if self._revision:\n            return self._revision\n        if self.req.revision:  # type: ignore[attr-defined]\n            return self.req.revision  # type: ignore[attr-defined]\n        return self._prepared.revision if self._prepared else \"unknown\"\n\n    def __repr__(self) -> str:\n        source = getattr(self.link, \"comes_from\", None)\n        from_source = f\" from {source}\" if source else \"\"\n        return f\"<Candidate {self}{from_source}>\"\n\n    def __str__(self) -> str:\n        if self.req.is_named:\n            return f\"{self.name}@{self.version}\"\n        assert self.link is not None\n        return f\"{self.name}@{self.link.url_without_fragment}\"\n\n    @classmethod\n    def from_installation_candidate(cls, candidate: Package, req: Requirement) -> Candidate:\n        \"\"\"Build a candidate from unearth's find result.\"\"\"\n        return cls(\n            req,\n            name=candidate.name,\n            version=str(candidate.version),\n            link=candidate.link,\n        )\n\n    @property\n    def requires_python(self) -> str:\n        \"\"\"The Python version constraint of the candidate.\"\"\"\n        if self._requires_python is not None:\n            return self._requires_python\n        if self.link:\n            requires_python = self.link.requires_python\n            if requires_python is not None:\n                if requires_python.isdigit():\n                    requires_python = f\">={requires_python},<{int(requires_python) + 1}\"\n                try:  # ensure the specifier is valid\n                    PySpecSet(requires_python)\n                except InvalidPyVersion:\n                    pass\n                else:\n                    self._requires_python = requires_python\n        return self._requires_python or \"\"\n\n    @requires_python.setter\n    def requires_python(self, value: str) -> None:\n        try:  # ensure the specifier is valid\n            PySpecSet(value)\n        except InvalidPyVersion:\n            return\n        self._requires_python = value\n\n    @no_type_check\n    def as_lockfile_entry(self, project_root: Path) -> dict[str, Any]:\n        \"\"\"Build a lockfile entry dictionary for the candidate.\"\"\"\n        version = str(self.version)\n        if self.req.is_pinned:\n            spec = next(iter(self.req.specifier))\n            should_normalize_version = \"+\" not in spec.version\n        else:\n            should_normalize_version = True\n\n        if should_normalize_version:\n            try:\n                version = str(comparable_version(version))\n            except InvalidVersion as e:\n                raise RequirementError(f\"Invalid version for {self.req.as_line()}: {e}\") from None\n        result = {\n            \"name\": normalize_name(self.name),\n            \"version\": version,\n            \"extras\": sorted(self.req.extras or ()),\n            \"requires_python\": str(self.requires_python),\n            \"editable\": self.req.editable,\n            \"subdirectory\": getattr(self.req, \"subdirectory\", None),\n        }\n        if self.req.is_vcs:\n            result.update(\n                {\n                    self.req.vcs: self.req.repo,\n                    \"ref\": self.req.ref,\n                }\n            )\n            if not self.req.editable:\n                result.update(revision=self.get_revision())\n        elif not self.req.is_named:\n            if self.req.is_file_or_url and self.req.is_local:\n                self.req._root = project_root\n                result.update(path=self.req.str_path)\n            else:\n                result.update(url=self.req.url)\n        return {k: v for k, v in result.items() if v}\n\n    def format(self) -> str:\n        \"\"\"Format for output.\"\"\"\n        return f\"[req]{self.name}[/] [warning]{self.version}[/]\"\n\n    def prepare(self, environment: BaseEnvironment, reporter: CandidateReporter | None = None) -> PreparedCandidate:\n        \"\"\"Prepare the candidate for installation.\"\"\"\n        if self._prepared is None:\n            if isinstance(self.req, FileRequirement):\n                self.req.check_installable()\n            self._prepared = PreparedCandidate(self, environment, reporter=reporter or CandidateReporter())\n        else:\n            self._prepared.environment = environment\n            if reporter is not None:\n                self._prepared.reporter = reporter\n        return self._prepared\n\n\n@dataclasses.dataclass\nclass PreparedCandidate:\n    \"\"\"A candidate that has been prepared for installation.\n    The metadata and built wheel are available.\n    \"\"\"\n\n    _build_dir_cache: ClassVar[dict[Link, str]] = {}\n\n    candidate: Candidate\n    environment: BaseEnvironment\n    reporter: CandidateReporter = dataclasses.field(default_factory=CandidateReporter)\n\n    def __post_init__(self) -> None:\n        self.req = self.candidate.req\n        self.link = self._replace_url_vars(self.candidate.link)\n\n        self._cached: Path | None = None\n        self._source_dir: Path | None = None\n        self._unpacked_dir: Path | None = None\n        self._metadata_dir: str | None = None\n        self._metadata: im.Distribution | None = None\n\n        if self.link is not None and self.link.is_file and self.link.file_path.is_dir():\n            self._source_dir = self.link.file_path\n            self._unpacked_dir = self._source_dir / (self.link.subdirectory or \"\")\n\n    def _replace_url_vars(self, link: Link | None) -> Link | None:\n        if link is None:\n            return None\n        url = self.environment.project.backend.expand_line(link.normalized)\n        return dataclasses.replace(link, url=url)\n\n    @cached_property\n    def revision(self) -> str:\n        from unearth import vcs_support\n\n        if not (self._source_dir and os.path.exists(self._source_dir)):\n            # It happens because the cached wheel is hit and the source code isn't\n            # pulled to local. In this case the link url must contain the full commit\n            # hash which can be taken as the revision safely.\n            # See more info at https://github.com/pdm-project/pdm/issues/349\n            rev = get_rev_from_url(self.candidate.link.url)  # type: ignore[union-attr]\n            if rev:\n                return rev\n        assert isinstance(self.req, VcsRequirement)\n        return vcs_support.get_backend(self.req.vcs, self.environment.project.core.ui.verbosity).get_revision(\n            cast(Path, self._source_dir)\n        )\n\n    def direct_url(self) -> dict[str, Any] | None:\n        \"\"\"PEP 610 direct_url.json data\"\"\"\n        req = self.req\n        if isinstance(req, VcsRequirement):\n            if req.editable:\n                assert self._source_dir\n                return _filter_none(\n                    {\n                        \"url\": self._source_dir.as_uri(),\n                        \"dir_info\": {\"editable\": True},\n                        \"subdirectory\": req.subdirectory,\n                    }\n                )\n            return _filter_none(\n                {\n                    \"url\": url_without_fragments(req.repo),\n                    \"vcs_info\": _filter_none(\n                        {\n                            \"vcs\": req.vcs,\n                            \"requested_revision\": req.ref,\n                            \"commit_id\": self.revision,\n                        }\n                    ),\n                    \"subdirectory\": req.subdirectory,\n                }\n            )\n        elif isinstance(req, FileRequirement):\n            assert self.link is not None\n            if self.link.is_file and self.link.file_path.is_dir():\n                return _filter_none(\n                    {\n                        \"url\": self.link.url_without_fragment,\n                        \"dir_info\": _filter_none({\"editable\": req.editable or None}),\n                        \"subdirectory\": req.subdirectory,\n                    }\n                )\n            hash_cache = self.environment.project.make_hash_cache()\n            return _filter_none(\n                {\n                    \"url\": self.link.url_without_fragment,\n                    \"archive_info\": {\n                        \"hash\": hash_cache.get_hash(self.link, self.environment.session).replace(\":\", \"=\")\n                    },\n                    \"subdirectory\": req.subdirectory,\n                }\n            )\n        else:\n            return None\n\n    def build(self) -> Path:\n        \"\"\"Call PEP 517 build hook to build the candidate into a wheel\"\"\"\n        self._obtain(allow_all=False)\n        if self._cached:\n            return self._cached\n        if not self.req.editable:\n            cached = self._get_build_cache()\n            if cached:\n                return cached\n        assert self._source_dir, \"Source directory isn't ready yet\"\n        builder_cls = EditableBuilder if self.req.editable else WheelBuilder\n        builder = builder_cls(str(self._unpacked_dir), self.environment)\n        build_dir = self._get_wheel_dir()\n        os.makedirs(build_dir, exist_ok=True)\n        termui.logger.info(\"Running PEP 517 backend to build a wheel for %s\", self.link)\n        self.reporter.report_build_start(self.link.filename)  # type: ignore[union-attr]\n        self._cached = Path(builder.build(build_dir, metadata_directory=self._metadata_dir))\n        self.reporter.report_build_end(self.link.filename)  # type: ignore[union-attr]\n        return self._cached\n\n    def _obtain(self, allow_all: bool = False, unpack: bool = True) -> None:\n        \"\"\"Fetch the link of the candidate and unpack to local if necessary.\n\n        :param allow_all: If true, don't validate the wheel tag nor hashes\n        :param unpack: Whether to download and unpack the link if it's not local\n        \"\"\"\n        if self._cached and self._wheel_compatible(self._cached.name, allow_all):\n            return\n\n        if self._source_dir and self._source_dir.exists():\n            return\n\n        sources = filtered_sources(self.environment.project.sources, self.req.key)\n        env_spec = self.environment.allow_all_spec if allow_all else self.environment.spec\n        with self.environment.get_finder(sources, env_spec=env_spec) as finder:\n            if not self.link or (self.link.is_wheel and not self._wheel_compatible(self.link.filename, allow_all)):\n                if self.req.is_file_or_url:\n                    raise CandidateNotFound(f\"The URL requirement {self.req.as_line()} is a wheel but incompatible\")\n                self.link = self._cached = None  # reset the incompatible wheel\n                self.link = _find_best_match_link(\n                    finder, self.req.as_pinned_version(self.candidate.version), self.candidate.hashes\n                )\n                if not self.link:\n                    raise CandidateNotFound(\n                        f\"No candidate is found for `{self.req.project_name}` that matches the environment or hashes\"\n                    )\n                if not self.candidate.link:\n                    self.candidate.link = self.link\n        # find if there is any build cache for the candidate\n        if not self.req.editable:\n            cached = self._get_build_cache()\n            if cached and self._wheel_compatible(cached.name, allow_all):\n                self._cached = cached\n                return\n        # If not, download and unpack the link\n        if unpack:\n            self._unpack(validate_hashes=not allow_all)\n\n    def _unpack(self, validate_hashes: bool = False) -> None:\n        hash_options = None\n        if validate_hashes and self.candidate.hashes:\n            hash_options = convert_hashes(self.candidate.hashes)\n        assert self.link is not None\n        with self.environment.get_finder() as finder:\n            with TemporaryDirectory(prefix=\"pdm-download-\") as tmpdir:\n                build_dir = self._get_build_dir()\n                if self.link.is_wheel:\n                    download_dir = build_dir\n                else:\n                    download_dir = tmpdir\n                result = finder.download_and_unpack(\n                    self.link,\n                    build_dir,\n                    download_dir,\n                    hash_options,\n                    download_reporter=self.reporter.report_download,\n                    unpack_reporter=self.reporter.report_unpack,\n                )\n        if self.link.is_wheel:\n            self._cached = result\n        else:\n            self._source_dir = Path(build_dir)\n            self._unpacked_dir = result\n\n    def prepare_metadata(self, force_build: bool = False) -> im.Distribution:\n        if self.candidate.installed is not None:\n            return self.candidate.installed\n\n        self._obtain(allow_all=True, unpack=False)\n        if self._metadata_dir:\n            return im.PathDistribution(Path(self._metadata_dir))\n\n        if self._cached:\n            return self._get_metadata_from_wheel(self._cached)\n\n        assert self.link is not None\n        if self.link.dist_info_metadata:\n            assert self.link.dist_info_link\n            dist = self._get_metadata_from_metadata_link(self.link.dist_info_link, self.link.dist_info_metadata)\n            if dist is not None:\n                return dist\n\n        self._unpack(validate_hashes=False)\n        if self._cached:  # check again if the wheel is downloaded to local\n            return self._get_metadata_from_wheel(self._cached)\n\n        assert self._unpacked_dir, \"Source directory isn't ready yet\"\n        pyproject_toml = self._unpacked_dir / \"pyproject.toml\"\n        if not force_build and pyproject_toml.exists():\n            dist = self._get_metadata_from_project(pyproject_toml)\n            if dist is not None:\n                return dist\n\n        # If all fail, try building the source to get the metadata\n        metadata_parent = self.environment.project.core.create_temp_dir(prefix=\"pdm-meta-\")\n        return self._get_metadata_from_build(self._unpacked_dir, metadata_parent)\n\n    def _get_metadata_from_metadata_link(\n        self, link: Link, medata_hash: bool | dict[str, str] | None\n    ) -> im.Distribution | None:\n        resp = self.environment.session.get(link.normalized)\n        if isinstance(medata_hash, dict):\n            hash_name, hash_value = next(iter(medata_hash.items()))\n            if hashlib.new(hash_name, resp.content).hexdigest() != hash_value:\n                termui.logger.warning(\"Metadata hash mismatch for %s, ignoring the metadata\", link)\n                return None\n        return MetadataDistribution(resp.text)\n\n    def _get_metadata_from_wheel(self, wheel: Path) -> im.Distribution:\n        # Get metadata from METADATA inside the wheel\n        metadata_parent = self.environment.project.core.create_temp_dir(prefix=\"pdm-meta-\")\n        dist_info = self._metadata_dir = _get_wheel_metadata_from_wheel(wheel, metadata_parent)\n        return im.PathDistribution(Path(dist_info))\n\n    def _get_metadata_from_project(self, pyproject_toml: Path) -> im.Distribution | None:\n        # Try getting from PEP 621 metadata\n        from pdm.formats import MetaConvertError\n        from pdm.project.project_file import PyProject\n\n        try:\n            pyproject = PyProject(pyproject_toml, ui=self.environment.project.core.ui)\n        except MetaConvertError as e:\n            termui.logger.warning(\"Failed to parse pyproject.toml: %s\", e)\n            return None\n        metadata = pyproject.metadata\n        if not metadata:\n            termui.logger.warning(\"Failed to parse pyproject.toml\")\n            return None\n\n        dynamic_fields = metadata.get(\"dynamic\", [])\n        # Use the parse result only when all are static\n        if not set(dynamic_fields).isdisjoint(\n            {\n                \"name\",\n                \"version\",\n                \"dependencies\",\n                \"optional-dependencies\",\n                \"requires-python\",\n            }\n        ):\n            return None\n\n        try:\n            backend_cls = get_backend_by_spec(pyproject.build_system)\n        except Exception:\n            # no variable expansion\n            backend_cls = get_backend(\"setuptools\")\n        backend = backend_cls(pyproject_toml.parent)\n        if \"name\" not in metadata:\n            termui.logger.warning(\"Failed to parse pyproject.toml, name is required\")\n            return None\n        setup = Setup(\n            name=metadata.get(\"name\"),\n            summary=metadata.get(\"description\"),\n            version=metadata.get(\"version\", \"0.0.0\"),\n            install_requires=list(\n                map(\n                    backend.expand_line,\n                    metadata.get(\"dependencies\", []),\n                )\n            ),\n            extras_require={\n                k: list(map(backend.expand_line, v)) for k, v in metadata.get(\"optional-dependencies\", {}).items()\n            },\n            python_requires=metadata.get(\"requires-python\"),\n        )\n        return setup.as_dist()\n\n    def _get_metadata_from_build(self, source_dir: Path, metadata_parent: str) -> im.Distribution:\n        builder = EditableBuilder if self.req.editable else WheelBuilder\n        try:\n            termui.logger.info(\"Running PEP 517 backend to get metadata for %s\", self.link)\n            self.reporter.report_build_start(self.link.filename)  # type: ignore[union-attr]\n            self._metadata_dir = builder(source_dir, self.environment).prepare_metadata(metadata_parent)\n            self.reporter.report_build_end(self.link.filename)  # type: ignore[union-attr]\n        except BuildError:\n            termui.logger.warning(\"Failed to build package, try parsing project files.\")\n            try:\n                setup = Setup.from_directory(source_dir)\n            except Exception:\n                message = \"Failed to parse the project files, dependencies may be missing\"\n                termui.logger.warning(message)\n                warnings.warn(message, PDMWarning, stacklevel=1)\n                setup = Setup()\n            return setup.as_dist()\n        else:\n            return im.PathDistribution(Path(cast(str, self._metadata_dir)))\n\n    @property\n    def metadata(self) -> im.Distribution:\n        if self._metadata is None:\n            result = self.prepare_metadata()\n            if not self.candidate.name:\n                self.req.name = self.candidate.name = cast(str, result.metadata.get(\"Name\"))\n            if not self.candidate.version and result.metadata.get(\"Version\"):\n                self.candidate.version = result.version\n            if not self.candidate.requires_python:\n                self.candidate.requires_python = result.metadata.get(\"Requires-Python\", \"\")\n            self._metadata = result\n        return self._metadata\n\n    def get_dependencies_from_metadata(self) -> list[Requirement]:\n        \"\"\"Get the dependencies of a candidate from metadata.\"\"\"\n        extras = self.req.extras or ()\n        return filter_requirements_with_extras(self.metadata.requires or [], extras)\n\n    def should_cache(self) -> bool:\n        \"\"\"Determine whether to cache the dependencies and built wheel.\"\"\"\n        from unearth import vcs_support\n\n        if not self.environment.project.core.state.enable_cache:\n            return False\n\n        link, source_dir = self.candidate.link, self._source_dir\n        if self.req.editable:\n            return False\n        if self.req.is_named:\n            return True\n        if self.req.is_vcs:\n            if not source_dir:\n                # If the candidate isn't prepared, we can't cache it\n                return False\n            assert link\n            vcs_backend = vcs_support.get_backend(link.vcs, self.environment.project.core.ui.verbosity)\n            return vcs_backend.is_immutable_revision(source_dir, link)\n        if link and not (link.is_file and link.file_path.is_dir()):\n            # Cache if the link contains egg-info like 'foo-1.0'\n            return _egg_info_re.search(link.filename) is not None\n        return False\n\n    def _get_build_cache(self) -> Path | None:\n        if not self.environment.project.core.state.enable_cache:\n            return None\n        wheel_cache = self.environment.project.make_wheel_cache()\n        assert self.candidate.link\n        cache_entry = wheel_cache.get(self.candidate.link, self.candidate.name, self.environment.spec)\n        if cache_entry is not None:\n            termui.logger.info(\"Using cached wheel: %s\", cache_entry)\n        return cache_entry\n\n    def _get_build_dir(self) -> str:\n        assert self.link is not None\n        if self.link.is_file and self.link.file_path.is_dir():\n            # Local directories are built in tree\n            return str(self.link.file_path)\n        if self.req.editable:\n            # In this branch the requirement must be an editable VCS requirement.\n            # The repository will be unpacked into a *persistent* src directory.\n            prefix: Path | None = None\n            if self.environment.is_local:\n                prefix = self.environment.packages_path  # type: ignore[attr-defined]\n            else:\n                venv = self.environment.interpreter.get_venv()\n                if venv is not None:\n                    prefix = venv.root\n            if prefix is not None:\n                src_dir = prefix / \"src\"\n            else:\n                src_dir = Path(\"src\")\n            src_dir.mkdir(exist_ok=True, parents=True)\n            dirname = self.candidate.name or self.req.name\n            if not dirname:\n                dirname, _ = os.path.splitext(self.link.filename)\n            return str(src_dir / str(dirname))\n        # Otherwise, for source dists, they will be unpacked into a *temp* directory.\n        if (build_dir := self._build_dir_cache.get(self.link)) is None:\n            build_dir = self._build_dir_cache[self.link] = self.environment.project.core.create_temp_dir(\n                prefix=\"pdm-build-\"\n            )\n        return build_dir\n\n    def _wheel_compatible(self, wheel_file: str, allow_all: bool = False) -> bool:\n        env_spec = self.environment.allow_all_spec if allow_all else self.environment.spec\n        return env_spec.wheel_compatibility(wheel_file) is not None\n\n    def _get_wheel_dir(self) -> str:\n        assert self.candidate.link\n        wheel_cache = self.environment.project.make_wheel_cache()\n        if self.should_cache():\n            termui.logger.info(\"Saving wheel to cache: %s\", self.candidate.link)\n            return wheel_cache.get_path_for_link(self.candidate.link, self.environment.spec).as_posix()\n        else:\n            return wheel_cache.get_ephemeral_path_for_link(self.candidate.link, self.environment.spec).as_posix()\n"
  },
  {
    "path": "src/pdm/models/finder.py",
    "content": "from __future__ import annotations\n\nimport logging\nfrom typing import TYPE_CHECKING, Any, Callable\n\nimport unearth\nfrom dep_logic.specifiers import InvalidSpecifier, parse_version_specifier\nfrom packaging.version import Version\nfrom unearth.evaluator import Evaluator, FormatControl, LinkMismatchError, Package\n\nfrom pdm.models.markers import EnvSpec\nfrom pdm.utils import parse_version\n\nlogger = logging.getLogger(\"unearth\")\n\nif TYPE_CHECKING:\n    from pdm.models.session import PDMPyPIClient\n\n\nclass ReverseVersion(Version):\n    \"\"\"A subclass of version that reverse the order of comparison.\"\"\"\n\n    def __lt__(self, other: Any) -> bool:\n        return super().__gt__(other)\n\n    def __le__(self, other: Any) -> bool:\n        return super().__ge__(other)\n\n    def __gt__(self, other: Any) -> bool:\n        return super().__lt__(other)\n\n    def __ge__(self, other: Any) -> bool:\n        return super().__le__(other)\n\n\nclass PDMEvaluator(Evaluator):\n    def __init__(self, *args: Any, env_spec: EnvSpec, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.env_spec = env_spec\n\n    def check_requires_python(self, link: unearth.Link) -> None:\n        if link.requires_python:\n            try:\n                requires_python = parse_version_specifier(link.requires_python)\n            except InvalidSpecifier as e:\n                logger.debug(\n                    \"Invalid requires-python specifier for link(%s) %s: %s\", link.redacted, link.requires_python, e\n                )\n                return\n            if (requires_python & self.env_spec.requires_python).is_empty():\n                raise LinkMismatchError(\n                    f\"The package requires-python {link.requires_python} is not compatible with the target {self.env_spec.requires_python}.\"\n                )\n\n    def check_wheel_tags(self, filename: str) -> None:\n        if self.env_spec.wheel_compatibility(filename) is None:\n            raise LinkMismatchError(\n                f\"The wheel file {filename} is not compatible with the target environment {self.env_spec}.\"\n            )\n\n\nclass PDMPackageFinder(unearth.PackageFinder):\n    def __init__(\n        self,\n        session: PDMPyPIClient | None = None,\n        *,\n        env_spec: EnvSpec,\n        minimal_version: bool = False,\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(session, **kwargs)\n        self.minimal_version = minimal_version\n        self.env_spec = env_spec\n\n    def build_evaluator(self, package_name: str, allow_yanked: bool = False) -> Evaluator:\n        format_control = FormatControl(no_binary=self.no_binary, only_binary=self.only_binary)\n        return PDMEvaluator(\n            package_name=package_name,\n            target_python=self.target_python,\n            allow_yanked=allow_yanked,\n            format_control=format_control,\n            exclude_newer_than=self.exclude_newer_than,\n            env_spec=self.env_spec,\n        )\n\n    def _sort_key(self, package: Package) -> tuple:\n        from packaging.utils import BuildTag, canonicalize_name\n\n        if self.minimal_version:\n            version_cls: Callable[[str], Version] = ReverseVersion\n        else:\n            version_cls = parse_version\n\n        link = package.link\n        compatibility = (0, 0, 0, 0)  # default value for sdists\n        build_tag: BuildTag = ()\n        prefer_binary = False\n        if link.is_wheel:\n            compat = self.env_spec.wheel_compatibility(link.filename)\n            if compat is None:\n                compatibility = (-1, -1, -1, -1)\n            else:\n                compatibility = compat\n            if canonicalize_name(package.name) in self.prefer_binary or \":all:\" in self.prefer_binary:\n                prefer_binary = True\n\n        return (\n            -int(link.is_yanked),\n            int(prefer_binary),\n            version_cls(package.version) if package.version is not None else version_cls(\"0\"),\n            compatibility,\n            build_tag,\n        )\n"
  },
  {
    "path": "src/pdm/models/in_process/__init__.py",
    "content": "\"\"\"\nA collection of functions that need to be called via a subprocess call.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport contextlib\nimport functools\nimport json\nimport os\nimport subprocess\nimport tempfile\nfrom typing import TYPE_CHECKING, Any, Generator\n\nfrom pdm.compat import resources_path\n\nif TYPE_CHECKING:\n    from pdm.models.markers import EnvSpec\n\n\n@contextlib.contextmanager\ndef _in_process_script(name: str) -> Generator[str, None, None]:\n    with resources_path(__name__, name) as script:\n        yield str(script)\n\n\ndef get_sys_config_paths(executable: str, vars: dict[str, str] | None = None, kind: str = \"default\") -> dict[str, str]:\n    \"\"\"Return the sys_config.get_paths() result for the python interpreter\"\"\"\n    env = os.environ.copy()\n    env.pop(\"__PYVENV_LAUNCHER__\", None)\n    if vars is not None:\n        env[\"_SYSCONFIG_VARS\"] = json.dumps(vars)\n\n    with _in_process_script(\"sysconfig_get_paths.py\") as script:\n        cmd = [executable, \"-Es\", script, kind]\n        return json.loads(subprocess.check_output(cmd, env=env))\n\n\ndef parse_setup_py(executable: str, path: str) -> dict[str, Any]:\n    \"\"\"Parse setup.py and return the kwargs\"\"\"\n    with _in_process_script(\"parse_setup.py\") as script:\n        _, outfile = tempfile.mkstemp(suffix=\".json\")\n        cmd = [executable, script, path, outfile]\n        subprocess.check_call(cmd)\n        with open(outfile, \"rb\") as fp:\n            return json.load(fp)\n\n\n@functools.lru_cache\ndef get_env_spec(executable: str) -> EnvSpec:\n    \"\"\"Get the environment spec of the python interpreter\"\"\"\n    from pdm.core import importlib_metadata\n    from pdm.models.markers import EnvSpec\n\n    required_libs = [\"dep_logic\", \"packaging\"]\n    shared_libs = {str(importlib_metadata.distribution(lib).locate_file(\"\")) for lib in required_libs}\n\n    with _in_process_script(\"env_spec.py\") as script:\n        return EnvSpec.from_spec(**json.loads(subprocess.check_output([executable, \"-EsS\", script, *shared_libs])))\n"
  },
  {
    "path": "src/pdm/models/in_process/env_spec.py",
    "content": "from __future__ import annotations\n\nimport json\nimport platform\nimport site\nimport sys\nimport sysconfig\n\n\ndef get_current_env_spec() -> dict[str, str | bool]:\n    from dep_logic.tags import Platform\n\n    python_version = f\"{sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]}\"\n    return {\n        \"requires_python\": f\"=={python_version}\",\n        \"platform\": str(Platform.current()),\n        \"implementation\": platform.python_implementation().lower(),\n        \"gil_disabled\": bool(sysconfig.get_config_var(\"Py_GIL_DISABLED\")),\n    }\n\n\nif __name__ == \"__main__\":\n    for shared_lib in sys.argv[1:]:\n        site.addsitedir(shared_lib)\n    print(json.dumps(get_current_env_spec(), indent=2))\n"
  },
  {
    "path": "src/pdm/models/in_process/parse_setup.py",
    "content": "from __future__ import annotations\n\nimport os\nimport sys\nfrom typing import Any\n\n\ndef _parse_setup_cfg(path: str) -> dict[str, Any]:\n    import configparser\n\n    setup_cfg = configparser.ConfigParser()\n    setup_cfg.read(path, encoding=\"utf-8\")\n\n    result: dict[str, Any] = {}\n    if not setup_cfg.has_section(\"metadata\"):\n        return result\n\n    metadata = setup_cfg[\"metadata\"]\n\n    if \"name\" in metadata:\n        result[\"name\"] = metadata[\"name\"]\n\n    if \"description\" in metadata:\n        result[\"description\"] = metadata[\"description\"]\n\n    if \"license\" in metadata:\n        result[\"license\"] = metadata[\"license\"]\n\n    if \"author\" in metadata:\n        result[\"author\"] = metadata[\"author\"]\n\n    if \"author_email\" in metadata:\n        result[\"author_email\"] = metadata[\"author_email\"]\n\n    if \"maintainer\" in metadata:\n        result[\"maintainer\"] = metadata[\"maintainer\"]\n\n    if \"maintainer_email\" in metadata:\n        result[\"maintainer_email\"] = metadata[\"maintainer_email\"]\n\n    if \"keywords\" in metadata:\n        keywords = metadata[\"keywords\"].strip().splitlines()\n        result[\"keywords\"] = keywords if len(keywords) > 1 else keywords[0]\n\n    if \"classifiers\" in metadata:\n        result[\"classifiers\"] = metadata[\"classifiers\"].strip().splitlines()\n\n    if \"url\" in metadata:\n        result[\"url\"] = metadata[\"url\"]\n\n    if \"download_url\" in metadata:\n        result[\"download_url\"] = metadata[\"download_url\"]\n\n    if \"project_urls\" in metadata:\n        result[\"project_urls\"] = dict(\n            [u.strip() for u in url.split(\"=\", 1)] for url in metadata[\"project_urls\"].strip().splitlines()\n        )\n\n    if \"long_description\" in metadata:\n        long_description = metadata[\"long_description\"].strip()\n        if long_description.startswith(\"file:\"):\n            result[\"readme\"] = long_description[5:].strip()\n\n    if setup_cfg.has_section(\"options\"):\n        options = setup_cfg[\"options\"]\n\n        if \"python_requires\" in options:\n            result[\"python_requires\"] = options[\"python_requires\"]\n\n        if \"install_requires\" in options:\n            result[\"install_requires\"] = options[\"install_requires\"].strip().splitlines()\n\n        if \"package_dir\" in options:\n            result[\"package_dir\"] = dict(\n                [p.strip() for p in d.split(\"=\", 1)] for d in options[\"package_dir\"].strip().splitlines()\n            )\n\n    if setup_cfg.has_section(\"options.extras_require\"):\n        result[\"extras_require\"] = {\n            feature: dependencies.strip().splitlines()\n            for feature, dependencies in setup_cfg[\"options.extras_require\"].items()\n        }\n\n    if setup_cfg.has_section(\"options.entry_points\"):\n        result[\"entry_points\"] = {\n            entry_point: definitions.strip().splitlines()\n            for entry_point, definitions in setup_cfg[\"options.entry_points\"].items()\n        }\n\n    return result\n\n\nsetup_kwargs = {}\nSUPPORTED_ARGS = (\n    \"name\",\n    \"version\",\n    \"description\",\n    \"license\",\n    \"author\",\n    \"author_email\",\n    \"maintainer\",\n    \"maintainer_email\",\n    \"keywords\",\n    \"classifiers\",\n    \"url\",\n    \"download_url\",\n    \"project_urls\",\n    \"python_requires\",\n    \"install_requires\",\n    \"extras_require\",\n    \"entry_points\",\n    \"package_dir\",\n)\n\n\ndef fake_setup(**kwargs):\n    setup_kwargs.update((k, v) for k, v in kwargs.items() if k in SUPPORTED_ARGS)\n\n\ndef clean_metadata(metadata: dict[str, Any]) -> None:\n    author = {}\n    if \"author\" in metadata:\n        author[\"name\"] = metadata.pop(\"author\")\n    if \"author_email\" in metadata:\n        author[\"email\"] = metadata.pop(\"author_email\")\n    if author:\n        metadata[\"authors\"] = [author]\n    maintainer = {}\n    if \"maintainer\" in metadata:\n        maintainer[\"name\"] = metadata.pop(\"maintainer\")\n    if \"maintainer_email\" in metadata:\n        maintainer[\"email\"] = metadata.pop(\"maintainer_email\")\n    if maintainer:\n        metadata[\"maintainers\"] = [maintainer]\n\n    urls = {}\n    if \"url\" in metadata:\n        urls[\"Homepage\"] = metadata.pop(\"url\")\n    if \"download_url\" in metadata:\n        urls[\"Downloads\"] = metadata.pop(\"download_url\")\n    if \"project_urls\" in metadata:\n        urls.update(metadata.pop(\"project_urls\"))\n    if urls:\n        metadata[\"urls\"] = urls\n\n    if \"\" in metadata.get(\"package_dir\", {}):\n        metadata[\"package_dir\"] = metadata[\"package_dir\"][\"\"]\n\n    if \"keywords\" in metadata:\n        keywords = metadata[\"keywords\"]\n        if isinstance(keywords, str):\n            keywords = [k.strip() for k in keywords.split(\",\")]\n        metadata[\"keywords\"] = keywords\n\n    if \"entry_points\" in metadata and isinstance(metadata[\"entry_points\"], dict):\n        entry_points = {}\n        for entry_point, definitions in metadata[\"entry_points\"].items():\n            if isinstance(definitions, str):\n                definitions = [definitions]\n            definitions = dict(sorted(d.replace(\" \", \"\").split(\"=\", 1) for d in definitions))\n\n            entry_points[entry_point] = definitions\n        if entry_points:\n            metadata[\"entry_points\"] = dict(sorted(entry_points.items()))\n\n\ndef parse_setup(path: str) -> dict[str, Any]:\n    import tokenize\n\n    parsed: dict[str, Any] = {}\n    path = os.path.abspath(path)\n    os.chdir(path)\n    if os.path.exists(\"setup.cfg\"):\n        parsed.update(_parse_setup_cfg(\"setup.cfg\"))\n\n    setup_path = os.path.join(path, \"setup.py\")\n    if os.path.exists(setup_path):\n        try:\n            import setuptools\n        except ModuleNotFoundError:\n            raise RuntimeError(\n                \"setuptools is required to convert setup.py, install it by `pdm add setuptools`\"\n            ) from None\n\n        setuptools.setup = fake_setup\n\n        # Execute setup.py and get the kwargs\n        __file__ = sys.argv[0] = setup_path\n        sys.path.insert(0, path)\n        setup_kwargs.clear()\n\n        with tokenize.open(setup_path) as f:\n            code = f.read()\n        exec(\n            compile(code, __file__, \"exec\"),\n            {\"__name__\": \"__main__\", \"__file__\": __file__, \"setup_kwargs\": setup_kwargs},\n        )\n        parsed.update(setup_kwargs)\n\n    if \"readme\" not in parsed:\n        for readme_file in (\"README.md\", \"README.rst\", \"README.txt\"):\n            readme_path = os.path.join(path, readme_file)\n            if os.path.exists(readme_path):\n                parsed[\"readme\"] = readme_file\n                break\n    clean_metadata(parsed)\n    return parsed\n\n\ndef json_default(o):\n    return \"<unserializable object>\"\n\n\nif __name__ == \"__main__\":\n    import json\n\n    outfile = sys.argv[2]\n    with open(outfile, \"w\") as f:\n        json.dump(parse_setup(sys.argv[1]), f, default=json_default)\n"
  },
  {
    "path": "src/pdm/models/in_process/sysconfig_get_paths.py",
    "content": "import json\nimport os\nimport sys\nimport sysconfig\n\n\ndef _running_under_venv():\n    \"\"\"This handles PEP 405 compliant virtual environments.\"\"\"\n    return sys.prefix != getattr(sys, \"base_prefix\", sys.prefix)\n\n\ndef _running_under_regular_virtualenv():\n    \"\"\"This handles virtual environments created with pypa's virtualenv.\"\"\"\n    # pypa/virtualenv case\n    return hasattr(sys, \"real_prefix\")\n\n\ndef running_under_virtualenv():\n    \"\"\"Return True if we're running inside a virtualenv, False otherwise.\"\"\"\n    return _running_under_venv() or _running_under_regular_virtualenv()\n\n\ndef _get_user_scheme():\n    if os.name == \"nt\":\n        return \"nt_user\"\n    if sys.platform == \"darwin\" and sys._framework:\n        return \"osx_framework_user\"\n    return \"posix_user\"\n\n\ndef get_paths(kind=\"default\", vars=None):\n    scheme_names = sysconfig.get_scheme_names()\n    if kind == \"user\" and not running_under_virtualenv():\n        scheme = _get_user_scheme()\n        if scheme not in scheme_names:\n            raise ValueError(f\"{scheme} is not a valid scheme on the system, or user site may be disabled.\")\n        return sysconfig.get_paths(scheme, vars=vars)\n    else:\n        if (\n            (sys.platform == \"darwin\" and \"osx_framework_library\" in scheme_names) or sys.platform == \"linux\"\n        ) and kind == \"prefix\":\n            return sysconfig.get_paths(\"posix_prefix\", vars=vars)\n        return sysconfig.get_paths(vars=vars)\n\n\ndef main():\n    vars = None\n    if \"_SYSCONFIG_VARS\" in os.environ:\n        vars = json.loads(os.environ[\"_SYSCONFIG_VARS\"])\n    kind = sys.argv[1] if len(sys.argv) > 1 else \"default\"\n    print(json.dumps(get_paths(kind, vars)))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "src/pdm/models/markers.py",
    "content": "from __future__ import annotations\n\nimport operator\nfrom dataclasses import dataclass, replace\nfrom functools import lru_cache, reduce\nfrom typing import TYPE_CHECKING, Any, cast, overload\n\nfrom dep_logic.markers import (\n    BaseMarker,\n    InvalidMarker,\n    MarkerExpression,\n    MarkerUnion,\n    MultiMarker,\n    from_pkg_marker,\n    parse_marker,\n)\nfrom dep_logic.tags import EnvSpec as _EnvSpec\nfrom dep_logic.tags import Implementation, Platform\nfrom packaging.markers import Marker as PackageMarker\n\nfrom pdm.exceptions import RequirementError\nfrom pdm.models.specifiers import PySpecSet\n\nif TYPE_CHECKING:\n    from typing import Self\n\n\nPLATFORM_MARKERS = frozenset(\n    {\"sys_platform\", \"platform_release\", \"platform_system\", \"platform_version\", \"os_name\", \"platform_machine\"}\n)\nIMPLEMENTATION_MARKERS = frozenset({\"implementation_name\", \"implementation_version\", \"platform_python_implementation\"})\nPYTHON_MARKERS = frozenset({\"python_version\", \"python_full_version\"})\n\n\ndef exclude_multi(marker: Marker, *names: str) -> Marker:\n    inner = marker.inner\n    for name in names:\n        inner = inner.exclude(name)\n    return type(marker)(inner)\n\n\n@dataclass(frozen=True, unsafe_hash=True, repr=False)\nclass Marker:\n    inner: BaseMarker\n\n    def __and__(self, other: Any) -> Marker:\n        if not isinstance(other, Marker):\n            return NotImplemented\n        return type(self)(self.inner & other.inner)\n\n    def __or__(self, other: Any) -> Marker:\n        if not isinstance(other, Marker):\n            return NotImplemented\n        return type(self)(self.inner | other.inner)\n\n    def is_any(self) -> bool:\n        return self.inner.is_any()\n\n    def is_empty(self) -> bool:\n        return self.inner.is_empty()\n\n    def __str__(self) -> str:\n        return str(self.inner)\n\n    def __repr__(self) -> str:\n        return f\"<Marker {self.inner}>\"\n\n    def evaluate(self, environment: dict[str, Any] | None = None) -> bool:\n        return self.inner.evaluate(environment)\n\n    def matches(self, spec: EnvSpec) -> bool:\n        non_python_marker, python_spec = self.split_pyspec()\n        if spec.platform is None:\n            non_python_marker = exclude_multi(non_python_marker, *PLATFORM_MARKERS)\n        if spec.implementation is None:\n            non_python_marker = exclude_multi(non_python_marker, *IMPLEMENTATION_MARKERS)\n        return not (python_spec & spec.requires_python).is_empty() and non_python_marker.evaluate(spec.markers())\n\n    @lru_cache(maxsize=1024)\n    def split_pyspec(self) -> tuple[Marker, PySpecSet]:\n        \"\"\"Split `python_version` and `python_full_version` from marker string\"\"\"\n        python_marker = self.inner.only(*PYTHON_MARKERS)\n        if python_marker.is_any():\n            return self, PySpecSet()\n        new_marker = exclude_multi(self, *PYTHON_MARKERS)\n        return new_marker, _build_pyspec_from_marker(python_marker)\n\n    def split_extras(self) -> tuple[Marker, Marker]:\n        \"\"\"An element can be stripped from the marker only if all parts are connected\n        with `and` operator. The rest part are returned as a string or `None` if all are\n        stripped.\n        \"\"\"\n        return type(self)(self.inner.without_extras()), type(self)(self.inner.only(\"extra\"))\n\n\n@overload\ndef get_marker(marker: None) -> None: ...\n\n\n@overload\ndef get_marker(marker: PackageMarker | Marker | str) -> Marker: ...\n\n\ndef get_marker(marker: PackageMarker | Marker | str | None) -> Marker | None:\n    if marker is None:\n        return None\n    if isinstance(marker, Marker):\n        return marker\n    elif isinstance(marker, PackageMarker):\n        return Marker(from_pkg_marker(marker))\n    try:\n        return Marker(parse_marker(marker))\n    except InvalidMarker as e:\n        raise RequirementError(f\"Invalid marker {marker}: {e}\") from e\n\n\ndef _build_pyspec_from_marker(marker: BaseMarker) -> PySpecSet:\n    def split_version(version: str) -> list[str]:\n        if \",\" in version:\n            return [v.strip() for v in version.split(\",\")]\n        return version.split()\n\n    if isinstance(marker, MarkerExpression):\n        name = marker.name\n        op = marker.op\n        version = marker.value\n        if name == \"python_version\":\n            if op == \">\":\n                int_versions = [int(ver) for ver in version.split(\".\")]\n                if len(int_versions) < 2:\n                    int_versions.append(0)\n                int_versions[-1] += 1\n                version = \".\".join(str(v) for v in int_versions)\n                op = \">=\"\n            elif op in (\"==\", \"!=\"):\n                if len(version.split(\".\")) < 3:\n                    version += \".*\"\n            elif op in (\"in\", \"not in\"):\n                version = \" \".join(v + \".*\" for v in split_version(version))\n        if op == \"in\":\n            pyspec = reduce(operator.or_, (PySpecSet(f\"=={v}\") for v in split_version(version)))\n        elif op == \"not in\":\n            pyspec = reduce(operator.and_, (PySpecSet(f\"!={v}\") for v in split_version(version)))\n        else:\n            pyspec = PySpecSet(f\"{op}{version}\")\n        return pyspec\n    elif isinstance(marker, MultiMarker):\n        return reduce(operator.and_, (_build_pyspec_from_marker(m) for m in marker.markers))\n    elif isinstance(marker, MarkerUnion):\n        return reduce(operator.or_, (_build_pyspec_from_marker(m) for m in marker.markers))\n    else:  # pragma: no cover\n        raise TypeError(f\"Unsupported marker type: {type(marker)}\")\n\n\nclass EnvSpec(_EnvSpec):\n    def replace(self, **kwargs: Any) -> Self:\n        if \"requires_python\" in kwargs:\n            kwargs[\"requires_python\"] = cast(PySpecSet, kwargs[\"requires_python\"])._logic\n        if \"platform\" in kwargs:\n            kwargs[\"platform\"] = Platform.parse(kwargs[\"platform\"])\n        if \"implementation\" in kwargs:\n            kwargs[\"implementation\"] = Implementation.parse(kwargs[\"implementation\"])\n        return replace(self, **kwargs)\n\n    def markers_with_defaults(self) -> dict[str, str]:\n        from packaging.markers import default_environment\n\n        return {**default_environment(), **self.markers()}  # type: ignore[dict-item]\n\n    @classmethod\n    def from_marker(cls, marker: Marker) -> Self:  # pragma: no cover\n        \"\"\"Create an EnvSpec from a Marker object.\"\"\"\n        marker_no_python, pyspec = marker.split_pyspec()\n        kwargs = {\"requires_python\": str(pyspec)}\n        if not marker_no_python.is_any() and not isinstance(marker_no_python.inner, (MultiMarker, MarkerExpression)):\n            raise TypeError(\n                f\"Unsupported environment marker: {marker}, not a single expression or connected with 'and'\"\n            )\n        if not (implementation_marker := marker_no_python.inner.only(\"implementation_name\")).is_any():\n            assert isinstance(implementation_marker, MarkerExpression)\n            kwargs[\"implementation\"] = implementation_marker.value\n        if not (platform_marker := marker_no_python.inner.only(*PLATFORM_MARKERS)).is_any():\n            platform = platform_marker.only(\"platform_system\")\n            arch = platform_marker.only(\"platform_machine\")\n            if not all(isinstance(m, MarkerExpression) for m in (platform, arch)):\n                raise TypeError(f\"Unsupported platform marker: {platform_marker}\")\n            os = platform.value.lower()\n            if os == \"linux\":\n                os = \"manylinux_2_17\"\n            elif os == \"darwin\":\n                os = \"macos\"\n            kwargs[\"platform\"] = f\"{os}_{arch.value.lower()}\"\n        return cls.from_spec(**kwargs)\n\n    def markers_with_python(self) -> Marker:\n        env = self.markers()\n        if self.platform is not None:  # pragma: no cover\n            env.update(\n                sys_platform=self.platform.sys_platform,\n                platform_system=self.platform.platform_system,\n                os_name=self.platform.os_name,\n                platform_machine=self.platform.platform_machine,\n            )\n        markers: list[BaseMarker] = []\n        env.pop(\"platform_release\", None)\n        env.pop(\"platform_version\", None)\n        env.pop(\"python_version\", None)\n        env.pop(\"python_full_version\", None)\n        markers.append(parse_marker(PySpecSet(self.requires_python).as_marker_string()))\n        for key, value in env.items():\n            markers.append(parse_marker(f'{key} == \"{value}\"'))\n        return Marker(MultiMarker.of(*markers))\n\n    def is_allow_all(self) -> bool:\n        return self.platform is None and self.implementation is None\n"
  },
  {
    "path": "src/pdm/models/project_info.py",
    "content": "from __future__ import annotations\n\nimport itertools\nfrom dataclasses import dataclass, field\nfrom email.message import Message\nfrom typing import TYPE_CHECKING, Any, Iterator, cast\n\nif TYPE_CHECKING:\n    from pdm.compat import Distribution\n\n\nDYNAMIC = \"DYNAMIC\"\n\n\n@dataclass\nclass ProjectInfo:\n    name: str\n    version: str\n    summary: str = \"\"\n    author: str = \"\"\n    email: str = \"\"\n    license: str = \"\"\n    requires_python: str = \"\"\n    platform: str = \"\"\n    keywords: str = \"\"\n    homepage: str = \"\"\n    project_urls: list[str] = field(default_factory=list)\n    latest_stable_version: str = \"\"\n    installed_version: str = \"\"\n\n    @classmethod\n    def from_distribution(cls, data: Distribution) -> ProjectInfo:\n        metadata = cast(Message, data.metadata)\n        keywords = metadata.get(\"Keywords\", \"\").replace(\",\", \", \")\n        platform = metadata.get(\"Platform\", \"\").replace(\",\", \", \")\n\n        if \"Project-URL\" in metadata:\n            project_urls = {\n                k.strip(): v.strip() for k, v in (row.split(\",\") for row in metadata.get_all(\"Project-URL\", []))\n            }\n        else:\n            project_urls = {}\n\n        return cls(\n            name=metadata.get(\"Name\", \"\"),\n            version=metadata.get(\"Version\", \"\"),\n            summary=metadata.get(\"Summary\", \"\"),\n            author=metadata.get(\"Author\", \"\"),\n            email=metadata.get(\"Author-email\", \"\"),\n            license=metadata.get(\"License\", \"\"),\n            requires_python=metadata.get(\"Requires-Python\", \"\"),\n            platform=platform,\n            keywords=keywords,\n            homepage=metadata.get(\"Home-page\", \"\"),\n            project_urls=[\": \".join(parts) for parts in project_urls.items()],\n        )\n\n    @classmethod\n    def from_metadata(cls, metadata: dict[str, Any]) -> ProjectInfo:\n        def get_str(key: str) -> str:\n            if key in metadata.get(\"dynamic\", []):\n                return DYNAMIC\n            return metadata.get(key, \"\")\n\n        authors = metadata.get(\"authors\", [])\n        author = authors[0][\"name\"] if authors else \"\"\n        email = authors[0][\"email\"] if authors else \"\"\n\n        return cls(\n            name=metadata[\"name\"],\n            version=get_str(\"version\"),\n            summary=get_str(\"description\"),\n            author=author,\n            email=email,\n            license=metadata.get(\"license\", {}).get(\"text\", \"\"),\n            requires_python=get_str(\"requires-python\"),\n            keywords=\",\".join(get_str(\"keywords\")),\n            project_urls=[\": \".join(parts) for parts in metadata.get(\"urls\", {}).items()],\n        )\n\n    def generate_rows(self) -> Iterator[tuple[str, str]]:\n        yield \"[primary]Name[/]:\", self.name\n        yield \"[primary]Latest version[/]:\", self.version\n        if self.latest_stable_version:\n            yield (\"[primary]Latest stable version[/]:\", self.latest_stable_version)\n        if self.installed_version:\n            yield (\"[primary]Installed version[/]:\", self.installed_version)\n        yield \"[primary]Summary[/]:\", self.summary\n        yield \"[primary]Requires Python:\", self.requires_python\n        yield \"[primary]Author[/]:\", self.author\n        yield \"[primary]Author email[/]:\", self.email\n        yield \"[primary]License[/]:\", self.license\n        yield \"[primary]Homepage[/]:\", self.homepage\n        yield from itertools.zip_longest((\"[primary]Project URLs[/]:\",), self.project_urls, fillvalue=\"\")\n        yield \"[primary]Platform[/]:\", self.platform\n        yield \"[primary]Keywords[/]:\", self.keywords\n"
  },
  {
    "path": "src/pdm/models/python.py",
    "content": "from __future__ import annotations\n\nimport os\nfrom functools import cached_property\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any\n\nfrom packaging.version import InvalidVersion, Version\n\nfrom pdm.models.venv import VirtualEnv\n\nif TYPE_CHECKING:\n    from findpython import PythonVersion\n\n\nclass PythonInfo:\n    \"\"\"\n    A convenient helper class that holds all information of a Python interpreter.\n    \"\"\"\n\n    def __init__(self, py_version: PythonVersion) -> None:\n        self._py_ver = py_version\n\n    @classmethod\n    def from_path(cls, path: str | Path) -> PythonInfo:\n        from findpython import PythonVersion\n\n        py_ver = PythonVersion(Path(path))\n        return cls(py_ver)\n\n    @cached_property\n    def valid(self) -> bool:\n        return self._py_ver.executable.exists() and self._py_ver.is_valid()\n\n    def __hash__(self) -> int:\n        return hash(self._py_ver)\n\n    def __eq__(self, o: Any) -> bool:\n        if not isinstance(o, PythonInfo):\n            return False\n        return self.path == o.path\n\n    @property\n    def path(self) -> Path:\n        return self._py_ver.executable\n\n    @property\n    def executable(self) -> Path:\n        return self._py_ver.interpreter\n\n    @cached_property\n    def version(self) -> Version:\n        return self._py_ver.version\n\n    @cached_property\n    def implementation(self) -> str:\n        return self._py_ver.implementation.lower()\n\n    @property\n    def major(self) -> int:\n        return self.version.major\n\n    @property\n    def minor(self) -> int:\n        return self.version.minor\n\n    @property\n    def micro(self) -> int:\n        return self.version.micro\n\n    @property\n    def version_tuple(self) -> tuple[int, ...]:\n        return (self.major, self.minor, self.micro)\n\n    @property\n    def is_32bit(self) -> bool:\n        return \"32bit\" in self._py_ver.architecture\n\n    def for_tag(self) -> str:\n        return f\"{self.major}{self.minor}\"\n\n    @property\n    def identifier(self) -> str:\n        try:\n            version_str = f\"{self.major}.{self.minor}\"\n        except InvalidVersion:\n            return \"unknown\"\n\n        if self._py_ver.freethreaded:\n            version_str += \"t\"\n        if os.name == \"nt\" and self.is_32bit:\n            version_str += \"-32\"\n        return version_str\n\n    def get_venv(self) -> VirtualEnv | None:\n        return VirtualEnv.from_interpreter(self.executable)\n"
  },
  {
    "path": "src/pdm/models/python_max_versions.json",
    "content": "{\n    \"2\": 7,\n    \"2.0\": 1,\n    \"2.1\": 3,\n    \"2.2\": 3,\n    \"2.3\": 7,\n    \"2.4\": 6,\n    \"2.5\": 6,\n    \"2.6\": 9,\n    \"2.7\": 18,\n    \"3\": 14,\n    \"3.0\": 1,\n    \"3.1\": 5,\n    \"3.10\": 19,\n    \"3.11\": 14,\n    \"3.12\": 12,\n    \"3.13\": 11,\n    \"3.14\": 2,\n    \"3.2\": 6,\n    \"3.3\": 7,\n    \"3.4\": 10,\n    \"3.5\": 10,\n    \"3.6\": 15,\n    \"3.7\": 17,\n    \"3.8\": 20,\n    \"3.9\": 25\n}\n"
  },
  {
    "path": "src/pdm/models/reporter.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any\n\nfrom rich import get_console\nfrom rich.live import Live\nfrom rich.progress import MofNCompleteColumn, Progress, SpinnerColumn, TaskProgressColumn, TimeElapsedColumn\n\nfrom pdm import termui\n\nif TYPE_CHECKING:\n    from rich.console import Console, ConsoleOptions, RenderResult\n    from rich.progress import Progress, TaskID\n\n\nclass CandidateReporter:\n    def report_download(self, link: Any, completed: int, total: int | None) -> None:\n        pass\n\n    def report_build_start(self, filename: str) -> None:\n        pass\n\n    def report_build_end(self, filename: str) -> None:\n        pass\n\n    def report_unpack(self, filename: Path, completed: int, total: int | None) -> None:\n        pass\n\n\n@dataclass\nclass RichProgressReporter(CandidateReporter):\n    progress: Progress\n    task_id: TaskID\n\n    def report_download(self, link: Any, completed: int, total: int | None) -> None:\n        self.progress.update(self.task_id, completed=completed, total=total, text=\"Downloading...\")\n\n    def report_unpack(self, filename: Path, completed: int, total: int | None) -> None:\n        self.progress.update(self.task_id, completed=completed, total=total, text=\"Unpacking...\")\n\n    def report_build_start(self, filename: str) -> None:\n        task = self.progress._tasks[self.task_id]\n        task.total = None\n        task.finished_time = None\n        self.progress.update(self.task_id, text=\"Building...\")\n\n    def report_build_end(self, filename: str) -> None:\n        self.progress.update(self.task_id, text=\"\")\n\n\nclass InstallationStatus:\n    def __init__(self, ui: termui.UI, text: str) -> None:\n        self.ui = ui\n        self.console = get_console()\n        self._spinner = Progress(\n            SpinnerColumn(termui.SPINNER),\n            TimeElapsedColumn(),\n            \"{task.description}\",\n            MofNCompleteColumn(),\n            console=self.console,\n        )\n        self._spinner_task = self._spinner.add_task(text, total=None)\n        self.progress = Progress(\n            \" \",\n            SpinnerColumn(termui.SPINNER, style=\"primary\"),\n            \"{task.description}\",\n            \"[info]{task.fields[text]}\",\n            TaskProgressColumn(\"[info]{task.percentage:>3.0f}%[/]\"),\n            console=self.console,\n        )\n        self.live = Live(self, console=self.console)\n\n    def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:\n        yield self.progress\n        yield \"\"\n        yield self._spinner\n\n    def update_spinner(\n        self,\n        *,\n        total: float | None = None,\n        completed: float | None = None,\n        advance: float | None = None,\n        description: str | None = None,\n    ) -> None:\n        if self.ui.verbosity >= termui.Verbosity.DETAIL and description is not None:\n            self.console.print(f\"  {description}\")\n        self._spinner.update(\n            self._spinner_task, total=total, completed=completed, advance=advance, description=description\n        )\n        self.live.refresh()\n\n    def start(self) -> None:\n        \"\"\"Start the progress display.\"\"\"\n        if self.ui.verbosity < termui.Verbosity.DETAIL:\n            self.live.start(refresh=True)\n\n    def stop(self) -> None:\n        \"\"\"Stop the progress display.\"\"\"\n        self.live.stop()\n        if not self.console.is_interactive:\n            self.console.print()\n\n    def __enter__(self) -> InstallationStatus:\n        self.start()\n        return self\n\n    def __exit__(self, *args: Any) -> None:\n        self.stop()\n"
  },
  {
    "path": "src/pdm/models/repositories/__init__.py",
    "content": "from pdm.models.repositories.base import BaseRepository as BaseRepository\nfrom pdm.models.repositories.base import CandidateMetadata as CandidateMetadata\nfrom pdm.models.repositories.lock import LockedRepository as LockedRepository\nfrom pdm.models.repositories.lock import Package as Package\nfrom pdm.models.repositories.pypi import PyPIRepository as PyPIRepository\n"
  },
  {
    "path": "src/pdm/models/repositories/base.py",
    "content": "from __future__ import annotations\n\nimport fnmatch\nimport re\nimport sys\nimport warnings\nfrom functools import wraps\nfrom typing import TYPE_CHECKING, Generator, NamedTuple, TypeVar, cast\n\nfrom pdm import termui\nfrom pdm._types import NotSet, NotSetType\nfrom pdm.exceptions import CandidateInfoNotFound, PackageWarning\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.markers import EnvSpec\nfrom pdm.models.requirements import Requirement, parse_line\nfrom pdm.models.specifiers import PySpecSet\nfrom pdm.utils import deprecation_warning, filtered_sources, normalize_name\n\nif TYPE_CHECKING:\n    from typing import Callable, Iterable\n\n    from unearth import Link\n\n    from pdm._types import FileHash, RepositoryConfig, SearchResults\n    from pdm.environments import BaseEnvironment\n\n\nT = TypeVar(\"T\", bound=\"BaseRepository\")\n\n\nclass CandidateMetadata(NamedTuple):\n    dependencies: list[Requirement]\n    requires_python: str\n    summary: str\n\n\ndef cache_result(func: Callable[[T, Candidate], CandidateMetadata]) -> Callable[[T, Candidate], CandidateMetadata]:\n    @wraps(func)\n    def wrapper(self: T, candidate: Candidate) -> CandidateMetadata:\n        result = func(self, candidate)\n        prepared = candidate.prepared\n        info = ([r.as_line() for r in result.dependencies], result.requires_python, result.summary)\n        if prepared and prepared.should_cache():\n            self._candidate_info_cache.set(candidate, info)\n        return result\n\n    return wrapper\n\n\nclass BaseRepository:\n    \"\"\"A Repository acts as the source of packages and metadata.\"\"\"\n\n    def __init__(\n        self,\n        sources: list[RepositoryConfig],\n        environment: BaseEnvironment,\n        ignore_compatibility: bool | NotSetType = NotSet,\n        env_spec: EnvSpec | None = None,\n    ) -> None:\n        \"\"\"\n        :param sources: a list of sources to download packages from.\n        :param environment: the bound environment instance.\n        :param ignore_compatibility: (DEPRECATED)if True, don't evaluate candidate against\n            the current environment.\n        :param env_spec: the environment specifier to filter the candidates.\n        \"\"\"\n        from pdm.resolver.reporters import LockReporter\n\n        self.sources = sources\n        self.environment = environment\n        self._candidate_info_cache = environment.project.make_candidate_info_cache()\n        self._hash_cache = environment.project.make_hash_cache()\n        self.has_warnings = False\n        if ignore_compatibility is not NotSet:  # pragma: no cover\n            deprecation_warning(\n                \"The ignore_compatibility argument is deprecated and will be removed in the future. \"\n                \"Pass in env_set instead. This repository doesn't support lock targets.\",\n                stacklevel=2,\n            )\n        else:\n            ignore_compatibility = True\n        if env_spec is None:  # pragma: no cover\n            if ignore_compatibility:\n                env_spec = environment.allow_all_spec\n            else:\n                env_spec = environment.spec\n        self.env_spec = env_spec\n        self.reporter = LockReporter()\n\n    def get_filtered_sources(self, req: Requirement) -> list[RepositoryConfig]:\n        \"\"\"Get matching sources based on the index attribute.\"\"\"\n        return filtered_sources(self.sources, req.key)\n\n    def get_dependencies(self, candidate: Candidate) -> tuple[list[Requirement], PySpecSet, str]:\n        \"\"\"Get (dependencies, python_specifier, summary) of the candidate.\"\"\"\n        requires_python, summary = \"\", \"\"\n        requirements: list[Requirement] = []\n        last_ext_info = None\n        for getter in self.dependency_generators():\n            try:\n                requirements, requires_python, summary = getter(candidate)\n            except CandidateInfoNotFound:\n                last_ext_info = sys.exc_info()\n                continue\n            break\n        else:\n            if last_ext_info is not None:\n                raise last_ext_info[1].with_traceback(last_ext_info[2])  # type: ignore[union-attr]\n\n        candidate.requires_python = requires_python\n        candidate.summary = summary\n        return requirements, PySpecSet(requires_python), summary\n\n    def _find_candidates(self, requirement: Requirement, minimal_version: bool) -> Iterable[Candidate]:\n        raise NotImplementedError\n\n    def is_this_package(self, requirement: Requirement) -> bool:\n        \"\"\"Whether the requirement is the same as this package\"\"\"\n        project = self.environment.project\n        return requirement.is_named and project.is_distribution and requirement.key == normalize_name(project.name)\n\n    def make_this_candidate(self, requirement: Requirement) -> Candidate:\n        \"\"\"Make a candidate for this package.\n        In this case the finder will look for a candidate from the package sources\n        \"\"\"\n        from unearth import Link\n\n        project = self.environment.project\n        link = Link.from_path(project.root)\n        candidate = Candidate(requirement, project.name, link=link)\n        with self.reporter.make_candidate_reporter(candidate) as reporter:\n            candidate.prepare(self.environment, reporter=reporter).metadata\n        return candidate\n\n    def _should_ignore_package_warning(self, requirement: Requirement) -> bool:\n        ignore_settings: list[str] = self.environment.project.pyproject.settings.get(\"ignore_package_warnings\", [])\n        package_name = requirement.key\n        assert package_name is not None\n        for pat in ignore_settings:\n            pat = re.sub(r\"[^A-Za-z0-9?*\\[\\]]+\", \"-\", pat).lower()\n            if fnmatch.fnmatch(package_name, pat):\n                return True\n        return False\n\n    def find_candidates(\n        self,\n        requirement: Requirement,\n        allow_prereleases: bool | None = None,\n        ignore_requires_python: bool = False,\n        minimal_version: bool = False,\n    ) -> Iterable[Candidate]:\n        \"\"\"Find candidates of the given NamedRequirement. Let it to be implemented in\n        subclasses.\n\n        :param requirement: the requirement to find\n        :param allow_prereleases: whether to include pre-releases\n        :param ignore_requires_python: whether to ignore the requires-python marker\n        :param minimal_version: whether to prefer the minimal versions of the package\n        \"\"\"\n        # `allow_prereleases` is None means to let the specifier decide whether to\n        # include prereleases\n        from unearth.utils import LazySequence\n\n        if self.is_this_package(requirement):\n            return [self.make_this_candidate(requirement)]\n        requires_python = requirement.requires_python & self.env_spec.requires_python\n        cans = LazySequence(self._find_candidates(requirement, minimal_version=minimal_version))\n        check_prereleases = allow_prereleases\n        if check_prereleases is None:\n            check_prereleases = requirement.specifier.prereleases is True\n        applicable_cans = LazySequence(\n            c\n            for c in cans\n            if requirement.specifier.contains(c.version, check_prereleases)  # type: ignore[arg-type, union-attr]\n        )\n\n        def filter_candidates_with_requires_python(candidates: Iterable[Candidate]) -> Generator[Candidate]:\n            env_requires_python = PySpecSet(self.env_spec.requires_python)\n            if ignore_requires_python:\n                yield from candidates\n                return\n\n            def python_specifier(spec: str | PySpecSet) -> str:\n                if isinstance(spec, PySpecSet):\n                    spec = str(spec)\n                return \"all Python versions\" if not spec else f\"Python{spec}\"\n\n            for candidate in candidates:\n                if not requires_python.is_subset(candidate.requires_python):\n                    if self._should_ignore_package_warning(requirement):\n                        continue\n                    working_requires_python = env_requires_python & PySpecSet(candidate.requires_python)\n                    if working_requires_python.is_empty():  # pragma: no cover\n                        continue\n                    warnings.warn(\n                        f\"Skipping {candidate.name}@{candidate.version} because it requires \"\n                        f\"{python_specifier(candidate.requires_python)} but the lock targets to work with \"\n                        f\"{python_specifier(env_requires_python)}. Instead, another version of \"\n                        f\"{candidate.name} that supports {python_specifier(env_requires_python)} will \"\n                        f\"be used.\\nIf you want to install {candidate.name}@{candidate.version}, \"\n                        \"narrow down the `requires-python` range to \"\n                        f'include this version. For example, \"{working_requires_python}\" should work.',\n                        PackageWarning,\n                        stacklevel=4,\n                    )\n                    self.has_warnings = True\n                else:\n                    yield candidate\n\n        applicable_cans_python_compatible = LazySequence(filter_candidates_with_requires_python(applicable_cans))\n        # Evaluate data-requires-python attr and discard incompatible candidates\n        # to reduce the number of candidates to resolve.\n        if applicable_cans_python_compatible:\n            applicable_cans = applicable_cans_python_compatible\n\n        if not applicable_cans:\n            termui.logger.debug(\"\\tCould not find any matching candidates.\")\n\n        if not applicable_cans and allow_prereleases is None:\n            # No non-pre-releases is found, force pre-releases now\n            applicable_cans = LazySequence(\n                c\n                for c in cans\n                if requirement.specifier.contains(c.version, True)  # type: ignore[arg-type, union-attr]\n            )\n            applicable_cans_python_compatible = LazySequence(filter_candidates_with_requires_python(applicable_cans))\n            if applicable_cans_python_compatible:\n                applicable_cans = applicable_cans_python_compatible\n\n            if not applicable_cans:\n                termui.logger.debug(\n                    \"\\tCould not find any matching candidates even when considering pre-releases.\",\n                )\n\n        def log_candidates(title: str, candidates: Iterable[Candidate], max_lines: int = 10) -> None:\n            termui.logger.debug(\"\\t\" + title)\n            logged_lines = set()\n            for can in candidates:\n                new_line = f\"\\t  {can!r}\"\n                if new_line not in logged_lines:\n                    logged_lines.add(new_line)\n                    if len(logged_lines) > max_lines:\n                        termui.logger.debug(\"\\t  ... [more]\")\n                        break\n                    else:\n                        termui.logger.debug(new_line)\n\n        if self.environment.project.core.ui.verbosity >= termui.Verbosity.DEBUG:\n            if applicable_cans:\n                log_candidates(\"Found matching candidates:\", applicable_cans)\n            elif cans:\n                log_candidates(\"Found but non-matching candidates:\", cans)\n\n        return applicable_cans\n\n    def _get_dependencies_from_cache(self, candidate: Candidate) -> CandidateMetadata:\n        try:\n            info = self._candidate_info_cache.get(candidate)\n        except KeyError:\n            raise CandidateInfoNotFound(candidate) from None\n\n        deps: list[Requirement] = []\n        for line in info[0]:\n            deps.append(parse_line(line))\n        termui.logger.debug(\"Using cached metadata for %s\", candidate)\n        return CandidateMetadata(deps, info[1], info[2])\n\n    @cache_result\n    def _get_dependencies_from_metadata(self, candidate: Candidate) -> CandidateMetadata:\n        with self.reporter.make_candidate_reporter(candidate) as reporter:\n            prepared = candidate.prepare(self.environment, reporter=reporter)\n            deps = prepared.get_dependencies_from_metadata()\n            requires_python = candidate.requires_python\n            summary = prepared.metadata.metadata.get(\"Summary\", \"\")\n        return CandidateMetadata(deps, requires_python, summary)\n\n    def get_hashes(self, candidate: Candidate) -> list[FileHash]:\n        \"\"\"Get hashes of all possible installable candidates\n        of a given package version.\n        \"\"\"\n        if (\n            candidate.req.is_vcs or (candidate.req.is_file_or_url and candidate.req.is_local_dir)  # type: ignore[attr-defined]\n        ):\n            return []\n        if candidate.hashes and candidate.req.is_named:\n            return candidate.hashes\n        req = candidate.req.as_pinned_version(candidate.version)\n        comes_from = candidate.link.comes_from if candidate.link else None\n        result: list[FileHash] = []\n        logged = False\n        respect_source_order = self.environment.project.pyproject.settings.get(\"resolution\", {}).get(\n            \"respect-source-order\", False\n        )\n        sources = self.get_filtered_sources(candidate.req)\n        if req.is_named and respect_source_order and comes_from:\n            sources = [s for s in sources if comes_from.startswith(cast(str, s.url))]\n\n        if req.is_file_or_url:\n            this_link = cast(\"Link\", candidate.prepare(self.environment).link)\n            links: list[Link] = [this_link]\n        else:  # the req must be a named requirement\n            with self.environment.get_finder(sources, env_spec=self.env_spec) as finder:\n                links = [package.link for package in finder.find_matches(req.as_line())]\n        for link in links:\n            if not link or link.is_vcs or (link.is_file and link.file_path.is_dir()):\n                # The links found can still be a local directory or vcs, skipping it.\n                continue\n            if not logged:\n                termui.logger.info(\"Fetching hashes for %s\", candidate)\n                logged = True\n            result.append(\n                {\n                    \"url\": link.url_without_fragment,\n                    \"file\": link.filename,\n                    \"hash\": self._hash_cache.get_hash(link, self.environment.session),\n                }\n            )\n        return result\n\n    def dependency_generators(self) -> Iterable[Callable[[Candidate], CandidateMetadata]]:\n        \"\"\"Return an iterable of getter functions to get dependencies, which will be\n        called one by one.\n        \"\"\"\n        raise NotImplementedError\n\n    def search(self, query: str) -> SearchResults:\n        \"\"\"Search package by name or summary.\n\n        :param query: query string\n        :returns: search result, a dictionary of name: package metadata\n        \"\"\"\n        raise NotImplementedError\n\n    def fetch_hashes(self, candidates: Iterable[Candidate]) -> None:\n        \"\"\"Fetch hashes for candidates in parallel\"\"\"\n        from concurrent.futures import ThreadPoolExecutor\n\n        def do_fetch(candidate: Candidate) -> None:\n            candidate.hashes = self.get_hashes(candidate)\n\n        with ThreadPoolExecutor() as executor:\n            executor.map(do_fetch, candidates)\n"
  },
  {
    "path": "src/pdm/models/repositories/lock.py",
    "content": "from __future__ import annotations\n\nimport dataclasses\nimport itertools\nimport posixpath\nfrom functools import cached_property\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Collection, cast\n\nfrom dep_logic.markers import AnyMarker, BaseMarker\n\nfrom pdm.cli.utils import normalize_name\nfrom pdm.exceptions import CandidateNotFound, PdmException\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.markers import EnvSpec, exclude_multi, get_marker\nfrom pdm.models.repositories.base import BaseRepository, CandidateMetadata\nfrom pdm.models.requirements import FileRequirement, Requirement, parse_line\nfrom pdm.utils import cd, url_to_path, url_without_fragments\n\nif TYPE_CHECKING:\n    from typing import Any, Callable, Iterable, Mapping\n\n    from pdm._types import FileHash, RepositoryConfig\n    from pdm.environments import BaseEnvironment\n\n    CandidateKey = tuple[str, str | None, str | None, bool]\n\n\n@dataclasses.dataclass(frozen=True)\nclass Package:\n    candidate: Candidate\n    dependencies: list[str] | None = None\n    summary: str = \"\"\n    marker: BaseMarker = dataclasses.field(default_factory=AnyMarker)\n\n\nclass LockedRepository(BaseRepository):\n    def __init__(\n        self,\n        lockfile: Mapping[str, Any],\n        sources: list[RepositoryConfig],\n        environment: BaseEnvironment,\n        env_spec: EnvSpec | None = None,\n    ) -> None:\n        super().__init__(sources, environment, env_spec=env_spec or environment.spec)\n        self.packages: dict[CandidateKey, Package] = {}\n        self.targets: list[EnvSpec] = []\n        self._read_lockfile(lockfile)\n\n    def add_package(self, package: Package) -> None:\n        self.packages[self._identify_candidate(package.candidate)] = package\n\n    @cached_property\n    def all_candidates(self) -> dict[str, list[Candidate]]:\n        \"\"\"Return a dict of all candidates grouped by the package name.\"\"\"\n        result: dict[str, list[Candidate]] = {}\n        for entry in self.packages.values():\n            result.setdefault(entry.candidate.identify(), []).append(entry.candidate)\n        return result\n\n    @property\n    def candidates(self) -> dict[str, Candidate]:\n        \"\"\"Return a dict of candidates for the current environment.\"\"\"\n        result: dict[str, Candidate] = {}\n        for candidates in self.all_candidates.values():\n            for can in candidates:\n                if can.req.marker:\n                    marker = exclude_multi(can.req.marker, \"extras\", \"dependency_groups\")\n                    if not marker.matches(self.env_spec):\n                        continue\n                result[can.identify()] = can\n        return result\n\n    def _read_lockfile(self, lockfile: Mapping[str, Any]) -> None:\n        if lockfile.get(\"lock-version\"):\n            return self._read_pylock(lockfile)\n        else:\n            return self._read_pdm_lock(lockfile)\n\n    def _read_pylock(self, lockfile: Mapping[str, Any]) -> None:\n        root = self.environment.project.root\n\n        if \"targets\" in lockfile.get(\"tool\", {}).get(\"pdm\", {}):\n            self.targets = [EnvSpec.from_spec(**t) for t in lockfile[\"tool\"][\"pdm\"][\"targets\"]]\n        else:\n            for marker in lockfile.get(\"environments\", []):  # pragma: no cover\n                self.targets.append(EnvSpec.from_marker(get_marker(cast(str, marker))))\n\n        with cd(root):\n            for package in lockfile.get(\"packages\", []):\n                group_marker = AnyMarker()\n                package_name = package.pop(\"name\")\n                req_dict: dict[str, str | bool] = {}\n                if \"version\" in package:\n                    req_dict[\"version\"] = f\"=={package['version']}\"\n                if \"marker\" in package:\n                    package_marker = get_marker(cast(str, package[\"marker\"]))\n                    req_marker = exclude_multi(package_marker, \"extras\", \"dependency_groups\")\n                    group_marker = package_marker.inner.only(\"extras\", \"dependency_groups\")\n                    if not req_marker.is_any():\n                        req_dict[\"marker\"] = str(req_marker)\n                if vcs := package.get(\"vcs\"):  # pragma: no cover\n                    req_dict[vcs[\"type\"]] = vcs[\"url\"]\n                    req_dict[\"ref\"] = vcs.get(\"requested-revision\")\n                    req_dict[\"revision\"] = vcs.get(\"commit-id\")\n                    req_dict[\"subdirectory\"] = vcs.get(\"subdirectory\")\n                elif directory := package.get(\"directory\"):  # pragma: no cover\n                    req_dict[\"path\"] = directory[\"path\"]\n                    req_dict[\"editable\"] = directory.get(\"editable\", False)\n                    req_dict[\"subdirectory\"] = directory.get(\"subdirectory\")\n                elif archive := package.get(\"archive\"):  # pragma: no cover\n                    req_dict[\"url\"] = archive.get(\"url\")\n                    req_dict[\"path\"] = archive.get(\"path\")\n                    req_dict[\"subdirectory\"] = archive.get(\"subdirectory\")\n                req = Requirement.from_req_dict(package_name, req_dict)\n                if req.is_file_or_url and req.path and not req.url:  # type: ignore[attr-defined]\n                    req.url = root.joinpath(req.path).as_uri()  # type: ignore[attr-defined]\n                candidate = Candidate(req=req, name=package_name, version=package.get(\"version\"))\n                candidate.requires_python = package.get(\"requires-python\", \"\")\n                for artifact in itertools.chain(\n                    package.get(\"wheels\", []), [sdist] if (sdist := package.get(\"sdist\")) else []\n                ):\n                    algo, hash_value = next(iter(artifact[\"hashes\"].items()))\n                    hash_item: FileHash = {\"hash\": f\"{algo}:{hash_value}\"}\n                    if \"url\" in artifact:\n                        hash_item[\"url\"] = artifact[\"url\"]\n                    if \"name\" in artifact:\n                        hash_item[\"file\"] = artifact[\"name\"]\n                    candidate.hashes.append(hash_item)\n                dependencies = package.get(\"tool\", {}).get(\"pdm\", {}).get(\"dependencies\")\n                self.packages[self._identify_candidate(candidate)] = Package(candidate, dependencies, \"\", group_marker)\n\n    def _read_pdm_lock(self, lockfile: Mapping[str, Any]) -> None:\n        from pdm.project.lockfile import FLAG_CROSS_PLATFORM, FLAG_STATIC_URLS\n\n        root = self.environment.project.root\n        static_urls = FLAG_STATIC_URLS in self.environment.project.lockfile.strategy\n        self.targets = [EnvSpec.from_spec(**t) for t in lockfile.get(\"metadata\", {}).get(\"targets\", [])]\n        if not self.targets and lockfile:  # pragma: no cover\n            # XXX: for reading old lockfiles, to be removed in the future\n            if FLAG_CROSS_PLATFORM in self.environment.project.lockfile.strategy:\n                self.targets.append(self.environment.allow_all_spec)\n            else:\n                self.targets.append(self.environment.spec)\n        with cd(root):\n            for package in lockfile.get(\"package\", []):\n                version = package.get(\"version\")\n                if version:\n                    package[\"version\"] = f\"=={version}\"\n                package_name = package.pop(\"name\")\n                req_dict = {\n                    k: v\n                    for k, v in package.items()\n                    if k not in (\"dependencies\", \"requires_python\", \"summary\", \"files\", \"targets\")\n                }\n                req = Requirement.from_req_dict(package_name, req_dict)\n                if req.is_file_or_url and req.path and not req.url:  # type: ignore[attr-defined]\n                    req.url = root.joinpath(req.path).as_uri()  # type: ignore[attr-defined]\n                can = Candidate(req, name=package_name, version=version)\n                can.hashes = package.get(\"files\", [])\n                if not static_urls and any(\"url\" in f for f in can.hashes):\n                    raise PdmException(\n                        \"Static URLs are not allowed in lockfile unless enabled by `pdm lock --static-urls`.\"\n                    )\n                can_id = self._identify_candidate(can)\n                can.requires_python = package.get(\"requires_python\", \"\")\n                entry = Package(\n                    can,\n                    package.get(\"dependencies\", []),\n                    package.get(\"summary\", \"\"),\n                )\n                self.packages[can_id] = entry\n\n    def _identify_candidate(self, candidate: Candidate) -> CandidateKey:\n        url: str | None = None\n        if not candidate.req.is_named and candidate.link is not None:\n            url = candidate.link.url_without_fragment\n            url = self.environment.project.backend.expand_line(cast(str, url))\n            if url.startswith(\"file://\"):\n                path = posixpath.normpath(url_to_path(url))\n                url = Path(path).as_uri()\n        return (\n            candidate.identify(),\n            candidate.version if not url else None,\n            url,\n            candidate.req.editable,\n        )\n\n    def _get_dependencies_from_lockfile(self, candidate: Candidate) -> CandidateMetadata:\n        err = (\n            f\"Missing package {candidate.identify()} from the lockfile, \"\n            \"the lockfile may be broken. Run `pdm lock --update-reuse` to fix it.\"\n        )\n        try:\n            entry = self.packages[self._identify_candidate(candidate)]\n        except KeyError as e:  # pragma: no cover\n            raise CandidateNotFound(err) from e\n\n        if entry.dependencies is None:\n            raise CandidateNotFound(f\"Missing dependencies from the lockfile for package {candidate.identify()}\")\n        # populate candidate metadata\n        if not candidate.name:\n            candidate.name = entry.candidate.name\n        if not candidate.version:\n            candidate.version = entry.candidate.version\n        if not candidate.requires_python:\n            candidate.requires_python = entry.candidate.requires_python\n        deps: list[Requirement] = []\n        for line in entry.dependencies:\n            deps.append(parse_line(line))\n        return CandidateMetadata(deps, candidate.requires_python, entry.summary)\n\n    def dependency_generators(self) -> Iterable[Callable[[Candidate], CandidateMetadata]]:\n        return (self._get_dependencies_from_lockfile,)\n\n    def _matching_entries(self, requirement: Requirement) -> Iterable[Package]:\n        for key, entry in self.packages.items():\n            can_req = entry.candidate.req\n            if requirement.name:\n                if key[0] != requirement.identify():\n                    continue\n            else:\n                assert isinstance(requirement, FileRequirement)\n                if not isinstance(can_req, FileRequirement):\n                    continue\n                if requirement.path and can_req.path:\n                    if requirement.path != can_req.path:\n                        continue\n                elif key[2] is not None and key[2] != url_without_fragments(requirement.url):\n                    continue\n\n            yield entry\n\n    def find_candidates(\n        self,\n        requirement: Requirement,\n        allow_prereleases: bool | None = None,\n        ignore_requires_python: bool = False,\n        minimal_version: bool = False,\n    ) -> Iterable[Candidate]:\n        if self.is_this_package(requirement):\n            candidate = self.make_this_candidate(requirement)\n            if candidate is not None:\n                yield candidate\n                return\n        for entry in self._matching_entries(requirement):\n            can = entry.candidate.copy_with(requirement)\n            if not requirement.name:\n                # make sure can.identify() won't return a randomly-generated name\n                requirement.name = can.name\n            yield can\n\n    def get_hashes(self, candidate: Candidate) -> list[FileHash]:\n        return candidate.hashes\n\n    def evaluate_candidates(self, groups: Collection[str], evaluate_markers: bool = True) -> Iterable[Package]:\n        extras, dependency_groups = self.environment.project.split_extras_groups(list(groups))\n        excludes = {normalize_name(k) for k in self.environment.project.pyproject.resolution.get(\"excludes\", [])}\n        for package in self.packages.values():\n            can = package.candidate\n            if package.candidate.req.key in excludes:\n                continue\n            if evaluate_markers and can.req.marker and not can.req.marker.matches(self.env_spec):\n                continue\n            if not package.marker.evaluate({\"extras\": set(extras), \"dependency_groups\": set(dependency_groups)}):\n                continue\n            if can.req.groups and not any(g in can.req.groups for g in groups):\n                continue\n            yield package\n\n    def merge_result(self, env_spec: EnvSpec, result: Iterable[Package]) -> None:\n        if env_spec not in self.targets:\n            self.targets.append(env_spec)\n        for entry in result:\n            key = self._identify_candidate(entry.candidate)\n            existing = self.packages.get(key)\n            if existing is None:\n                self.packages[key] = entry\n            else:\n                # merge markers\n                old_marker = existing.candidate.req.marker\n                if old_marker is None or entry.candidate.req.marker is None:\n                    new_marker = None\n                else:\n                    new_marker = old_marker | entry.candidate.req.marker\n                    bare_marker, py_spec = new_marker.split_pyspec()\n                    if py_spec.is_superset(self.environment.python_requires):\n                        new_marker = bare_marker\n                    if new_marker.is_any():\n                        new_marker = None\n                # merge groups\n                new_groups = list(set(existing.candidate.req.groups) | set(entry.candidate.req.groups))\n                existing.candidate.req = dataclasses.replace(\n                    existing.candidate.req, marker=new_marker, groups=new_groups\n                )\n                # merge file hashes\n                for file in entry.candidate.hashes:\n                    if file not in existing.candidate.hashes:\n                        existing.candidate.hashes.append(file)\n        # clear caches\n        if \"all_candidates\" in self.__dict__:\n            del self.__dict__[\"all_candidates\"]\n"
  },
  {
    "path": "src/pdm/models/repositories/pypi.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, cast\n\nfrom pdm.exceptions import CandidateInfoNotFound, CandidateNotFound\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.repositories.base import BaseRepository, CandidateMetadata, cache_result\nfrom pdm.models.requirements import Requirement, filter_requirements_with_extras\nfrom pdm.models.search import SearchResultParser\n\nif TYPE_CHECKING:\n    from typing import Callable, Iterable\n\n    from pdm._types import SearchResults\n\n\nclass PyPIRepository(BaseRepository):\n    \"\"\"Get package and metadata from PyPI source.\"\"\"\n\n    DEFAULT_INDEX_URL = \"https://pypi.org\"\n\n    @cache_result\n    def _get_dependencies_from_json(self, candidate: Candidate) -> CandidateMetadata:  # pragma: no cover\n        if not candidate.name or not candidate.version:\n            # Only look for json api for named requirements.\n            raise CandidateInfoNotFound(candidate)\n        sources = self.get_filtered_sources(candidate.req)\n        url_prefixes = [\n            proc_url[:-7]  # Strip \"/simple\".\n            for proc_url in (raw_url.rstrip(\"/\") for raw_url in (source.url for source in sources) if raw_url)\n            if proc_url.endswith(\"/simple\")\n        ]\n        session = self.environment.session\n        for prefix in url_prefixes:\n            json_url = f\"{prefix}/pypi/{candidate.name}/{candidate.version}/json\"\n            resp = session.get(json_url)\n            if resp.is_error:\n                continue\n\n            info = resp.json()[\"info\"]\n\n            requires_python = info[\"requires_python\"] or \"\"\n            summary = info[\"summary\"] or \"\"\n            try:\n                requirement_lines = info[\"requires_dist\"] or []\n            except KeyError:\n                requirement_lines = info[\"requires\"] or []\n            requirements = filter_requirements_with_extras(requirement_lines, candidate.req.extras or ())\n            return CandidateMetadata(requirements, requires_python, summary)\n        raise CandidateInfoNotFound(candidate)\n\n    def dependency_generators(self) -> Iterable[Callable[[Candidate], CandidateMetadata]]:\n        yield self._get_dependencies_from_cache\n        if self.environment.project.config[\"pypi.json_api\"]:\n            yield self._get_dependencies_from_json\n        yield self._get_dependencies_from_metadata\n\n    def _find_candidates(self, requirement: Requirement, minimal_version: bool) -> Iterable[Candidate]:\n        from unearth.utils import LazySequence\n\n        sources = self.get_filtered_sources(requirement)\n        req_name = cast(str, requirement.project_name)\n        with self.environment.get_finder(sources, env_spec=self.env_spec, minimal_version=minimal_version) as finder:\n            cans = LazySequence(\n                Candidate.from_installation_candidate(c, requirement)\n                for c in finder.find_all_packages(req_name, allow_yanked=requirement.is_pinned)\n            )\n        if not cans:\n            raise CandidateNotFound(\n                f\"Unable to find candidates for {req_name}. There may \"\n                \"exist some issues with the package name or network condition.\"\n            )\n        return cans\n\n    def search(self, query: str) -> SearchResults:\n        pypi_simple = self.sources[0].url.rstrip(\"/\")  # type: ignore[union-attr]\n\n        if pypi_simple.endswith(\"/simple\"):\n            search_url = pypi_simple[:-6] + \"search\"\n        else:\n            search_url = pypi_simple + \"/search\"\n\n        session = self.environment.session\n        resp = session.get(search_url, params={\"q\": query})\n        if resp.status_code == 404:  # pragma: no cover\n            self.environment.project.core.ui.warn(\n                f\"{pypi_simple!r} doesn't support '/search' endpoint, fallback \"\n                f\"to {self.DEFAULT_INDEX_URL!r} now.\\n\"\n                \"This may take longer depending on your network condition.\",\n            )\n            resp = session.get(f\"{self.DEFAULT_INDEX_URL}/search/\", params={\"q\": query}, follow_redirects=True)\n        parser = SearchResultParser()\n        resp.raise_for_status()\n        parser.feed(resp.text)\n        return parser.results\n"
  },
  {
    "path": "src/pdm/models/requirements.py",
    "content": "from __future__ import annotations\n\nimport dataclasses\nimport functools\nimport inspect\nimport json\nimport os\nimport posixpath\nimport re\nimport secrets\nimport urllib.parse as urlparse\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, Sequence, TypeVar, cast\n\nfrom packaging.requirements import InvalidRequirement\nfrom packaging.requirements import Requirement as PackageRequirement\nfrom packaging.specifiers import InvalidSpecifier, SpecifierSet\nfrom packaging.utils import parse_sdist_filename, parse_wheel_filename\n\nfrom pdm.compat import Distribution\nfrom pdm.exceptions import RequirementError\nfrom pdm.models.backends import BuildBackend, get_relative_path\nfrom pdm.models.markers import Marker, get_marker\nfrom pdm.models.setup import Setup\nfrom pdm.models.specifiers import PySpecSet, fix_legacy_specifier, get_specifier\nfrom pdm.utils import (\n    add_ssh_scheme_to_git_uri,\n    comparable_version,\n    normalize_name,\n    split_path_fragments,\n    url_to_path,\n    url_without_fragments,\n)\n\nif TYPE_CHECKING:\n    from unearth import Link\n\n    from pdm._types import RequirementDict\n\n\nVCS_SCHEMA = (\"git\", \"hg\", \"svn\", \"bzr\")\n_vcs_req_re = re.compile(\n    rf\"(?P<url>(?P<vcs>{'|'.join(VCS_SCHEMA)})\\+[^\\s;]+)(?P<marker>[\\t ]*;[^\\n]+)?\",\n    flags=re.IGNORECASE,\n)\n_file_req_re = re.compile(\n    r\"(?:(?P<url>\\S+://[^\\s\\[\\];]+)|\"\n    r\"(?P<path>(?:[^\\s;\\[\\]]|\\\\ )*\"\n    r\"|'(?:[^']|\\\\')*'\"\n    r\"|\\\"(?:[^\\\"]|\\\\\\\")*\\\"))\"\n    r\"(?P<extras>\\[[^\\[\\]]+\\])?(?P<marker>[\\t ]*;[^\\n]+)?\"\n)\n_egg_info_re = re.compile(r\"([a-z0-9_.]+)-([a-z0-9_.!+-]+)\", re.IGNORECASE)\nT = TypeVar(\"T\", bound=\"Requirement\")\nALLOW_ANY = SpecifierSet()\n\n\ndef strip_extras(line: str) -> tuple[str, tuple[str, ...] | None]:\n    match = re.match(r\"^(.+?)(?:\\[([^\\]]+)\\])?$\", line)\n    assert match is not None\n    name, extras_str = match.groups()\n    extras = tuple({e.strip() for e in extras_str.split(\",\")}) if extras_str else None\n    return name, extras\n\n\n@functools.lru_cache(maxsize=None)\ndef _get_random_key(req: Requirement) -> str:\n    return f\":empty:{secrets.token_urlsafe(8)}\"\n\n\n@dataclasses.dataclass(eq=False)\nclass Requirement:\n    \"\"\"Base class of a package requirement.\n    A requirement is a (virtual) specification of a package which contains\n    some constraints of version, python version, or other marker.\n    \"\"\"\n\n    name: str | None = None\n    marker: Marker | None = None\n    extras: Sequence[str] | None = None\n    specifier: SpecifierSet = ALLOW_ANY\n    editable: bool = False\n    prerelease: bool | None = None\n    groups: list[str] = dataclasses.field(default_factory=list)\n\n    def __post_init__(self) -> None:\n        self.requires_python = self.marker.split_pyspec()[1] if self.marker else PySpecSet()\n\n    @property\n    def project_name(self) -> str | None:\n        return normalize_name(self.name, lowercase=False) if self.name else None\n\n    @property\n    def key(self) -> str | None:\n        return self.project_name.lower() if self.project_name else None\n\n    @property\n    def is_pinned(self) -> bool:\n        if len(self.specifier) != 1:\n            return False\n\n        sp = next(iter(self.specifier))\n        return sp.operator == \"===\" or (sp.operator == \"==\" and \"*\" not in sp.version)\n\n    def as_pinned_version(self: T, other_version: str | None) -> T:\n        \"\"\"Return a new requirement with the given pinned version.\"\"\"\n        if self.is_pinned or not other_version:\n            return self\n        normalized = comparable_version(other_version)\n        return dataclasses.replace(self, specifier=get_specifier(f\"=={normalized}\"))\n\n    def _hash_key(self) -> tuple:\n        return (\n            self.key,\n            frozenset(self.extras) if self.extras else None,\n            str(self.marker) if self.marker else None,\n        )\n\n    def __hash__(self) -> int:\n        return hash(self._hash_key())\n\n    def __eq__(self, o: object) -> bool:\n        return isinstance(o, Requirement) and self._hash_key() == o._hash_key()\n\n    def identify(self) -> str:\n        if not self.key:\n            return _get_random_key(self)\n        extras = \"[{}]\".format(\",\".join(sorted(self.extras))) if self.extras else \"\"\n        return self.key + extras\n\n    def __repr__(self) -> str:\n        return f\"<{self.__class__.__name__} {self.as_line()}>\"\n\n    def __str__(self) -> str:\n        return self.as_line()\n\n    @classmethod\n    def create(cls: type[T], **kwargs: Any) -> T:\n        if \"marker\" in kwargs:\n            kwargs[\"marker\"] = get_marker(kwargs[\"marker\"])\n        if \"extras\" in kwargs and isinstance(kwargs[\"extras\"], str):\n            kwargs[\"extras\"] = tuple(e.strip() for e in kwargs[\"extras\"][1:-1].split(\",\"))\n        version = kwargs.pop(\"version\", \"\")\n        try:\n            kwargs[\"specifier\"] = get_specifier(version)\n        except InvalidSpecifier as e:\n            raise RequirementError(f\"Invalid specifier for {kwargs.get('name')}: {version}: {e}\") from None\n        return cls(**{k: v for k, v in kwargs.items() if k in inspect.signature(cls).parameters})\n\n    @classmethod\n    def from_dist(cls, dist: Distribution) -> Requirement:\n        direct_url_json = dist.read_text(\"direct_url.json\")\n        if direct_url_json is not None:\n            direct_url = json.loads(direct_url_json)\n            data = {\n                \"name\": dist.metadata.get(\"Name\"),\n                \"url\": direct_url.get(\"url\"),\n                \"editable\": direct_url.get(\"dir_info\", {}).get(\"editable\"),\n                \"subdirectory\": direct_url.get(\"subdirectory\"),\n            }\n            if \"vcs_info\" in direct_url:\n                vcs_info = direct_url[\"vcs_info\"]\n                data.update(\n                    url=f\"{vcs_info['vcs']}+{direct_url['url']}\",\n                    ref=vcs_info.get(\"requested_revision\"),\n                    revision=vcs_info.get(\"commit_id\"),\n                )\n                return VcsRequirement.create(**data)\n            return FileRequirement.create(**data)\n        return NamedRequirement.create(name=dist.metadata[\"Name\"], version=f\"=={dist.version}\")\n\n    @classmethod\n    def from_req_dict(cls, name: str, req_dict: RequirementDict) -> Requirement:\n        if isinstance(req_dict, str):  # Version specifier only.\n            return NamedRequirement.create(name=name, version=req_dict)\n        for vcs in VCS_SCHEMA:\n            if vcs in req_dict:\n                repo = cast(str, req_dict.pop(vcs, None))\n                url = f\"{vcs}+{repo}\"\n                return VcsRequirement.create(name=name, vcs=vcs, url=url, **req_dict)\n        if \"path\" in req_dict or \"url\" in req_dict:\n            return FileRequirement.create(name=name, **req_dict)\n        return NamedRequirement.create(name=name, **req_dict)\n\n    @property\n    def is_named(self) -> bool:\n        return isinstance(self, NamedRequirement)\n\n    @property\n    def is_vcs(self) -> bool:\n        return isinstance(self, VcsRequirement)\n\n    @property\n    def is_file_or_url(self) -> bool:\n        return type(self) is FileRequirement\n\n    def as_line(self) -> str:\n        raise NotImplementedError\n\n    def matches(self, line: str) -> bool:\n        \"\"\"Return whether the passed in PEP 508 string\n        is the same requirement as this one.\n        \"\"\"\n        if not isinstance(line, str):\n            return False\n        req = parse_line(line)\n        return self.key == req.key or (\n            isinstance(self, FileRequirement) and isinstance(req, FileRequirement) and self.url == req.url\n        )\n\n    @classmethod\n    def from_pkg_requirement(cls, req: PackageRequirement) -> Requirement:\n        from unearth import Link\n\n        kwargs = {\n            \"name\": req.name,\n            \"extras\": req.extras,\n            \"specifier\": req.specifier,\n            \"marker\": get_marker(req.marker),\n        }\n        if getattr(req, \"url\", None):\n            link = Link(cast(str, req.url))\n            klass = VcsRequirement if link.is_vcs else FileRequirement\n            return klass(url=cast(str, req.url), **kwargs)  # type: ignore[arg-type]\n        else:\n            return NamedRequirement(**kwargs)  # type: ignore[arg-type]\n\n    def _format_marker(self) -> str:\n        if self.marker:\n            return f\"; {self.marker!s}\"\n        return \"\"\n\n\n@dataclasses.dataclass(eq=False)\nclass NamedRequirement(Requirement):\n    def as_line(self) -> str:\n        extras = f\"[{','.join(sorted(self.extras))}]\" if self.extras else \"\"\n        return f\"{self.project_name}{extras}{self.specifier or ''}{self._format_marker()}\"\n\n\n# Cache for checked paths to avoid checking the same path multiple times\n_checked_paths: set[Path] = set()\n\n\n@dataclasses.dataclass(eq=False)\nclass FileRequirement(Requirement):\n    url: str = \"\"\n    path: Path | None = None\n    subdirectory: str | None = None\n    _root: Path = dataclasses.field(default_factory=Path.cwd, repr=False)\n\n    def __post_init__(self) -> None:\n        super().__post_init__()\n        self._parse_url()\n\n    def _hash_key(self) -> tuple:\n        return (*super()._hash_key(), self.get_full_url(), self.editable)\n\n    def guess_name(self) -> str | None:\n        filename = os.path.basename(urlparse.unquote(url_without_fragments(self.url))).rsplit(\"@\", 1)[0]\n        if self.is_vcs:\n            if self.vcs == \"git\":  # type: ignore[attr-defined]\n                name = filename\n                if name.endswith(\".git\"):\n                    name = name[:-4]\n                return name\n            elif self.vcs == \"hg\":  # type: ignore[attr-defined]\n                return filename\n            else:  # svn and bzr\n                name, in_branch, _ = filename.rpartition(\"/branches/\")\n                if not in_branch and name.endswith(\"/trunk\"):\n                    return name[:-6]\n                return name\n        elif filename.endswith(\".whl\"):\n            return parse_wheel_filename(filename)[0]\n        else:\n            try:\n                return parse_sdist_filename(filename)[0]\n            except ValueError:\n                match = _egg_info_re.match(filename)\n                # Filename is like `<name>-<version>.tar.gz`, where name will be\n                # extracted and version will be left to be determined from\n                # the metadata.\n                if match:\n                    return match.group(1)\n        return None\n\n    @classmethod\n    def create(cls: type[T], **kwargs: Any) -> T:\n        if kwargs.get(\"path\"):\n            kwargs[\"path\"] = Path(kwargs[\"path\"])\n        return super().create(**kwargs)\n\n    @property\n    def str_path(self) -> str | None:\n        if not self.path:\n            return None\n        if self.path.is_absolute():\n            try:\n                result = self.path.relative_to(self._root).as_posix()\n            except ValueError:\n                return self.path.as_posix()\n        else:\n            result = self.path.as_posix()\n        result = posixpath.normpath(result)\n        if not result.startswith((\"./\", \"../\")) and result != \".\":\n            result = \"./\" + result\n        if result.startswith(\"./../\"):\n            result = result[2:]\n        return result\n\n    def _parse_url(self) -> None:\n        if self.path:\n            path, fragments = split_path_fragments(self.path)\n            if not self.url and path.is_absolute():\n                self.url = path.as_uri() + fragments\n                self.path = path\n        else:\n            url = url_without_fragments(self.url)\n            relpath = get_relative_path(url)\n            if relpath is None:\n                try:\n                    self.path = Path(url_to_path(url))\n                except ValueError:\n                    pass\n            else:\n                self.path = Path(relpath)\n\n        if self.path is not None and self.absolute_path not in _checked_paths:\n            # For relative path, we don't resolve URL now, so the path may still contain fragments,\n            # it will be handled in `relocate()` method.\n            assert self.absolute_path is not None\n            # There is a risk of infinite recursion here if the project file contains file dependencies\n            # referring to itself, mark the path as being checked to avoid that.\n            _checked_paths.add(self.absolute_path)\n            result = Setup.from_directory(self.absolute_path)\n            if result.name:\n                self.name = result.name\n            _checked_paths.discard(self.absolute_path)\n\n        if self.url:\n            self._parse_name_from_url()\n\n    def relocate(self, backend: BuildBackend) -> None:\n        \"\"\"Change the project root to the given path\"\"\"\n        if self.path is None or self.path.is_absolute():\n            return\n        # self.path is relative\n        path, fragments = split_path_fragments(self.path)\n        self.path = Path(os.path.relpath(path, backend.root))\n        relpath = self.path.as_posix()\n        if relpath == \".\":\n            relpath = \"\"\n        self.url = backend.relative_path_to_url(relpath) + fragments\n        self._root = backend.root\n\n    @property\n    def absolute_path(self) -> Path | None:\n        return self._root.joinpath(self.path) if self.path else None\n\n    @property\n    def is_local(self) -> bool:\n        return (path := self.absolute_path) is not None and path.exists()\n\n    @property\n    def is_local_dir(self) -> bool:\n        return self.is_local and cast(Path, self.absolute_path).is_dir()\n\n    def as_file_link(self) -> Link:\n        from unearth import Link\n\n        url = self.get_full_url()\n        # only subdirectory is useful in a file link\n        if self.subdirectory:\n            url += f\"#subdirectory={self.subdirectory}\"\n        return Link(url)\n\n    def get_full_url(self) -> str:\n        return url_without_fragments(self.url)\n\n    def as_line(self) -> str:\n        project_name = f\"{self.project_name}\" if self.project_name else \"\"\n        extras = f\"[{','.join(sorted(self.extras))}]\" if self.extras and self.project_name else \"\"\n        marker = self._format_marker()\n        if marker:\n            marker = f\" {marker}\"\n        url = self.get_full_url()\n        fragments = []\n        if self.subdirectory:\n            fragments.append(f\"subdirectory={self.subdirectory}\")\n        if self.editable:\n            if project_name:\n                fragments.insert(0, f\"egg={project_name}{extras}\")\n            fragment_str = (\"#\" + \"&\".join(fragments)) if fragments else \"\"\n            return f\"-e {url}{fragment_str}{marker}\"\n        delimiter = \" @ \" if project_name else \"\"\n        fragment_str = (\"#\" + \"&\".join(fragments)) if fragments else \"\"\n        return f\"{project_name}{extras}{delimiter}{url}{fragment_str}{marker}\"\n\n    def _parse_name_from_url(self) -> None:\n        parsed = urlparse.urlparse(self.url)\n        fragments = dict(urlparse.parse_qsl(parsed.fragment))\n        if \"egg\" in fragments:\n            egg_info = urlparse.unquote(fragments[\"egg\"])\n            name, extras = strip_extras(egg_info)\n            self.name = name\n            if not self.extras:\n                self.extras = extras\n        if not self.name and not self.is_vcs:\n            self.name = self.guess_name()\n\n    def check_installable(self) -> None:\n        if path := self.absolute_path:\n            if not path.exists():\n                raise RequirementError(f\"The local path '{self.path}' does not exist.\")\n            if path.is_dir():\n                if not path.joinpath(\"setup.py\").exists() and not path.joinpath(\"pyproject.toml\").exists():\n                    raise RequirementError(f\"The local path '{self.path}' is not installable.\")\n            elif self.editable:\n                raise RequirementError(\"Local file requirement must not be editable.\")\n\n\n@dataclasses.dataclass(eq=False)\nclass VcsRequirement(FileRequirement):\n    vcs: str = \"\"\n    ref: str | None = None\n    revision: str | None = None\n\n    def __post_init__(self) -> None:\n        super().__post_init__()\n        if not self.vcs:\n            self.vcs = self.url.split(\"+\", 1)[0]\n\n    def get_full_url(self) -> str:\n        url = super().get_full_url()\n        if self.revision and not self.editable:\n            url += f\"@{self.revision}\"\n        elif self.ref:\n            url += f\"@{self.ref}\"\n        return url\n\n    def _parse_url(self) -> None:\n        vcs, url_no_vcs = self.url.split(\"+\", 1)\n        if url_no_vcs.startswith(\"git@\"):\n            url_no_vcs = add_ssh_scheme_to_git_uri(url_no_vcs)\n        if not self.name:\n            self._parse_name_from_url()\n        ref = self.ref\n        parsed = urlparse.urlparse(url_no_vcs)\n        path = parsed.path\n        fragments = dict(urlparse.parse_qsl(parsed.fragment))\n        if \"subdirectory\" in fragments:\n            self.subdirectory = fragments[\"subdirectory\"]\n        if \"@\" in parsed.path:\n            path, ref = parsed.path.split(\"@\", 1)\n        repo = urlparse.urlunparse(parsed._replace(path=path, fragment=\"\"))\n        self.url = f\"{vcs}+{repo}\"\n        self.repo, self.ref = repo, ref\n\n\ndef filter_requirements_with_extras(\n    requirement_lines: list[str], extras: Sequence[str], include_default: bool = False\n) -> list[Requirement]:\n    \"\"\"Filter the requirements with extras.\n    If extras are given, return those with matching extra markers.\n    Otherwise, return those without extra markers.\n    \"\"\"\n    result: list[Requirement] = []\n    for req in requirement_lines:\n        _r = parse_requirement(req)\n        req_extras = get_marker(\"\")\n        if _r.marker:\n            rest, req_extras = _r.marker.split_extras()\n            _r.marker = rest if not rest.is_any() else None\n            if not req_extras.evaluate({\"extra\": extras or \"\"}):\n                continue\n        # Add to the requirements if:\n        # The requirement has no extras while requested extras are empty or include_default is True, or\n        # The requirement has extras, in which case the `evaluate()` test must have been passed.\n        if not req_extras.is_any() or include_default or not extras:\n            result.append(_r)\n\n    return result\n\n\ndef parse_as_pkg_requirement(line: str) -> PackageRequirement:\n    \"\"\"Parse a requirement line as packaging.requirement.Requirement\"\"\"\n    try:\n        return PackageRequirement(line)\n    except InvalidRequirement:\n        new_line = fix_legacy_specifier(line)\n        return PackageRequirement(new_line)\n\n\ndef parse_line(line: str) -> Requirement:\n    if line.startswith(\"-e \"):\n        return parse_requirement(line[3:].strip(), editable=True)\n    return parse_requirement(line)\n\n\ndef parse_requirement(line: str, editable: bool = False) -> Requirement:\n    m = _vcs_req_re.match(line)\n    r: Requirement\n    if m is not None:\n        r = VcsRequirement.create(**m.groupdict())\n    else:\n        # Special handling for hatch local references:\n        # https://hatch.pypa.io/latest/config/dependency/#local\n        # We replace the {root.uri} temporarily with a dummy URL header\n        # to make it pass through the packaging.requirement parser\n        # and then revert it.\n        root_url = Path().absolute().as_uri()\n        replaced = \"{root:uri}\" in line\n        if replaced:\n            line = line.replace(\"{root:uri}\", root_url)\n        try:\n            pkg_req = parse_as_pkg_requirement(line)\n        except InvalidRequirement as e:\n            m = _file_req_re.match(line)\n            if m is None:\n                raise RequirementError(f\"{line}: {e}\") from None\n            args = m.groupdict()\n            if not line.startswith(\".\") and not args[\"url\"] and args[\"path\"] and not os.path.exists(args[\"path\"]):\n                raise RequirementError(f\"{line}: {e}\") from None\n            r = FileRequirement.create(**args)\n        else:\n            r = Requirement.from_pkg_requirement(pkg_req)\n        if replaced:\n            assert isinstance(r, FileRequirement)\n            r.url = r.url.replace(root_url, \"{root:uri}\")\n            r.path = Path(get_relative_path(r.url) or \"\")\n\n    if editable:\n        if isinstance(r, FileRequirement) and (r.is_vcs or not r.url or r.url.startswith(\"file://\")):\n            r.editable = True\n        else:\n            raise RequirementError(f\"{line}: Editable requirement is only supported for VCS link or local directory.\")\n    return r\n"
  },
  {
    "path": "src/pdm/models/search.py",
    "content": "from __future__ import annotations\n\nimport functools\nfrom dataclasses import dataclass\nfrom html.parser import HTMLParser\nfrom typing import Callable\n\nfrom pdm._types import SearchResult\n\n\n@dataclass\nclass Result:\n    name: str = \"\"\n    version: str = \"\"\n    description: str = \"\"\n\n    def as_frozen(self) -> SearchResult:\n        return SearchResult(self.name, self.version, self.description)\n\n\nclass SearchResultParser(HTMLParser):\n    \"\"\"A simple HTML parser for pypi.org search results.\"\"\"\n\n    def __init__(self) -> None:\n        super().__init__()\n        self.results: list[SearchResult] = []\n        self._current: Result | None = None\n        self._nest_anchors = 0\n        self._data_callback: Callable[[str], None] | None = None\n\n    @staticmethod\n    def _match_class(attrs: list[tuple[str, str | None]], name: str) -> bool:\n        attrs_map = dict(attrs)\n        return name in (attrs_map.get(\"class\") or \"\").split()\n\n    def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:\n        if not self._current:\n            if tag == \"a\" and self._match_class(attrs, \"package-snippet\"):\n                self._current = Result()\n                self._nest_anchors = 1\n        else:\n            if tag == \"span\" and self._match_class(attrs, \"package-snippet__name\"):\n                self._data_callback = functools.partial(setattr, self._current, \"name\")\n            elif tag == \"span\" and self._match_class(attrs, \"package-snippet__version\"):\n                self._data_callback = functools.partial(setattr, self._current, \"version\")\n            elif tag == \"p\" and self._match_class(attrs, \"package-snippet__description\"):\n                self._data_callback = functools.partial(setattr, self._current, \"description\")\n            elif tag == \"a\":\n                self._nest_anchors += 1\n\n    def handle_data(self, data: str) -> None:\n        if self._data_callback is not None:\n            self._data_callback(data)\n            self._data_callback = None\n\n    def handle_endtag(self, tag: str) -> None:\n        if tag != \"a\" or self._current is None:\n            return\n        self._nest_anchors -= 1\n        if self._nest_anchors == 0:\n            if self._current.name:\n                self.results.append(self._current.as_frozen())\n            self._current = None\n"
  },
  {
    "path": "src/pdm/models/session.py",
    "content": "from __future__ import annotations\n\nimport os\nimport sqlite3\nimport sys\nimport threading\nfrom contextlib import closing\nfrom functools import lru_cache\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, cast\n\nimport hishel\nimport hishel.httpx\nimport httpx\nfrom unearth.fetchers import PyPIClient\n\nfrom pdm.__version__ import __version__\nfrom pdm.termui import logger\n\nif TYPE_CHECKING:\n    from ssl import SSLContext\n\n    from pdm._types import RepositoryConfig\n\n\ndef _create_truststore_ssl_context() -> SSLContext | None:\n    if sys.version_info < (3, 10):\n        return None\n\n    try:\n        import ssl\n    except ImportError:\n        return None\n\n    try:\n        import truststore\n    except ImportError:\n        return None\n\n    import certifi\n\n    ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)\n    ctx.load_verify_locations(certifi.where())\n    return ctx\n\n\n_ssl_context = _create_truststore_ssl_context()\nCACHES_TTL = 7 * 24 * 60 * 60  # 7 days\nMAX_RETRIES = 4\n\n\n@lru_cache(maxsize=None)\ndef _get_transport(\n    verify: bool | SSLContext | str = True,\n    cert: tuple[str, str | None] | None = None,\n    proxy: httpx.Proxy | None = None,\n) -> httpx.BaseTransport:\n    return httpx.HTTPTransport(verify=verify, cert=cert, trust_env=True, proxy=proxy, retries=MAX_RETRIES)\n\n\nclass ThreadedSyncSqliteStorage(hishel.SyncSqliteStorage):\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        self._local_conns: dict[int, sqlite3.Connection] = {}\n        self._lock = threading.Lock()\n        self._initialized = False\n        super().__init__(*args, **kwargs)\n\n    @property\n    def connection(self) -> sqlite3.Connection | None:\n        return self._local_conns.get(threading.get_ident())\n\n    @connection.setter\n    def connection(self, conn: sqlite3.Connection | None) -> None:\n        if conn is not None:\n            self._local_conns[threading.get_ident()] = conn\n\n    def close(self) -> None:\n        with self._lock:\n            while self._local_conns:\n                _, conn = self._local_conns.popitem()\n                conn.close()\n\n    def _ensure_connection(self) -> sqlite3.Connection:\n        \"\"\"\n        Ensure connection is established and database is initialized.\n\n        We need to create connections with check_same_thread=False for the sake of close(),\n        so we have to open-code the entire implementation here.\n        \"\"\"\n\n        # we take a lock _despite_ using TLS to protect against concurrent close(), otherwise we have a TOCTOU\n        # this is kinda ugly and defeats the purpose of using TLS, but eh\n        with self._lock:\n            if self.connection is None:\n                # Create cache directory and resolve full path on first connection\n                self.database_path.parent.mkdir(parents=True, exist_ok=True)\n                full_path = self.database_path.resolve()\n                conn = sqlite3.connect(str(full_path), check_same_thread=False)\n                with closing(conn.cursor()) as cursor:\n                    cursor.execute(\"PRAGMA foreign_keys=ON\")\n                self.connection = conn\n            if not self._initialized:\n                self._initialize_database()\n                self._initialized = True\n            return self.connection\n\n\nclass PDMPyPIClient(PyPIClient):\n    def __init__(self, *, sources: list[RepositoryConfig], cache_dir: Path | None = None, **kwargs: Any) -> None:\n        import shutil\n\n        from httpx._utils import URLPattern\n        from unearth.fetchers.sync import LocalFSTransport\n\n        if cache_dir is None:\n\n            def cache_transport(transport: httpx.BaseTransport) -> httpx.BaseTransport:\n                return transport\n        else:\n            # clean up old (pre-hishel 1.0) cache\n            cache_db = cache_dir / \"http-cache.db\"\n            if not cache_db.exists():\n                for f in cache_dir.iterdir():\n                    if not f.name.startswith(\"http-cache.db\"):\n                        if f.is_dir():\n                            shutil.rmtree(f, ignore_errors=True)\n                        else:\n                            f.unlink()\n            storage = ThreadedSyncSqliteStorage(database_path=cache_db, default_ttl=CACHES_TTL)\n\n            def cache_transport(transport: httpx.BaseTransport) -> httpx.BaseTransport:\n                return hishel.httpx.SyncCacheTransport(next_transport=transport, storage=storage)\n\n        mounts: dict[str, httpx.BaseTransport] = {\"file://\": LocalFSTransport()}\n        self._trusted_host_ports: set[tuple[str, int | None]] = set()\n        self._proxy_map = {\n            URLPattern(key): proxy for key, proxy in self._get_proxy_map(None, allow_env_proxies=True).items()\n        }\n        self._proxy_map = dict(sorted(self._proxy_map.items()))\n        for s in sources:\n            assert s.url is not None\n            url = httpx.URL(s.url)\n            if s.verify_ssl is False:\n                self._trusted_host_ports.add((url.host, url.port))\n            if s.name == \"pypi\":\n                kwargs[\"transport\"] = self._transport_for(s)\n                continue\n            mounts[f\"{url.scheme}://{url.netloc.decode('ascii')}/\"] = cache_transport(self._transport_for(s))\n        mounts.update(kwargs.pop(\"mounts\", None) or {})\n        kwargs.update(follow_redirects=True)\n\n        httpx.Client.__init__(self, mounts=mounts, **kwargs)\n\n        self.headers[\"User-Agent\"] = self._make_user_agent()\n        self.event_hooks[\"response\"].append(self.on_response)\n        self._transport = cache_transport(self._transport)  # type: ignore[has-type]\n\n    def _transport_for(self, source: RepositoryConfig) -> httpx.BaseTransport:\n        if source.verify_ssl is False:\n            verify: str | bool | SSLContext = False\n        elif source.ca_certs:\n            verify = source.ca_certs\n        else:\n            verify = os.getenv(\"REQUESTS_CA_BUNDLE\") or os.getenv(\"CURL_CA_BUNDLE\") or _ssl_context or True\n        if source.client_cert:\n            cert = (source.client_cert, source.client_key)\n        else:\n            cert = None\n        source_url = httpx.URL(cast(str, source.url))\n        proxy = next((proxy for pattern, proxy in self._proxy_map.items() if pattern.matches(source_url)), None)\n        return _get_transport(verify=verify, cert=cert, proxy=proxy)\n\n    def _make_user_agent(self) -> str:\n        import platform\n\n        return f\"pdm/{__version__} {platform.python_implementation()}/{platform.python_version()} {platform.system()}/{platform.release()}\"\n\n    def on_response(self, response: httpx.Response) -> None:\n        from unearth.utils import ARCHIVE_EXTENSIONS\n\n        if response.extensions.get(\"from_cache\"):\n            response.from_cache = True  # type: ignore[attr-defined]\n            if response.url.path.endswith(ARCHIVE_EXTENSIONS):\n                logger.info(\"Using cached response for %s\", response.url)\n"
  },
  {
    "path": "src/pdm/models/setup.py",
    "content": "from __future__ import annotations\n\nimport ast\nimport os\nfrom configparser import ConfigParser\nfrom dataclasses import asdict, dataclass, field, fields\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, Iterable, no_type_check\n\nfrom pdm.compat import Distribution\n\nif TYPE_CHECKING:\n    from importlib.metadata import _SimplePath\n\n\n@dataclass\nclass Setup:\n    \"\"\"\n    Abstraction of a Python project setup file.\n    \"\"\"\n\n    name: str | None = None\n    version: str | None = None\n    install_requires: list[str] = field(default_factory=list)\n    extras_require: dict[str, list[str]] = field(default_factory=dict)\n    python_requires: str | None = None\n    summary: str | None = None\n\n    def update(self, other: Setup) -> None:\n        for f in fields(self):\n            other_field = getattr(other, f.name)\n            if other_field:\n                setattr(self, f.name, other_field)\n\n    def as_dict(self) -> dict[str, Any]:\n        return asdict(self)\n\n    @classmethod\n    def from_directory(cls, dir: Path) -> Setup:\n        return _SetupReader.read_from_directory(dir)\n\n    def as_dist(self) -> Distribution:\n        return SetupDistribution(self)\n\n\nclass _SetupReader:\n    \"\"\"\n    Class that reads a setup.py file without executing it.\n    \"\"\"\n\n    @classmethod\n    def read_from_directory(cls, directory: Path) -> Setup:\n        result = Setup()\n\n        for filename, file_reader in [\n            (\"pyproject.toml\", cls.read_pyproject_toml),\n            (\"setup.cfg\", cls.read_setup_cfg),\n            (\"setup.py\", cls.read_setup_py),\n        ]:\n            filepath = directory / filename\n            if not filepath.exists():\n                continue\n\n            new_result = file_reader(filepath)\n            result.update(new_result)\n\n        return result\n\n    @staticmethod\n    def read_pyproject_toml(file: Path) -> Setup:\n        from pdm import termui\n        from pdm.exceptions import ProjectError\n        from pdm.formats import MetaConvertError\n        from pdm.project.project_file import PyProject\n\n        try:\n            metadata = PyProject(file, ui=termui.UI()).metadata\n        except ProjectError:\n            return Setup()\n        except MetaConvertError as e:\n            termui.logger.warning(\"Error parsing pyproject.toml, metadata may be incomplete. %s\", e)\n            metadata = e.data\n        return Setup(\n            name=metadata.get(\"name\"),\n            summary=metadata.get(\"description\"),\n            version=metadata.get(\"version\"),\n            install_requires=metadata.get(\"dependencies\", []),\n            extras_require=metadata.get(\"optional-dependencies\", {}),\n            python_requires=metadata.get(\"requires-python\"),\n        )\n\n    @no_type_check\n    @classmethod\n    def read_setup_py(cls, file: Path) -> Setup:\n        with file.open(encoding=\"utf-8\") as f:\n            content = f.read()\n\n        body = ast.parse(content).body\n\n        setup_call, body = cls._find_setup_call(body)\n        if not setup_call:\n            return Setup()\n\n        return Setup(\n            name=cls._find_single_string(setup_call, body, \"name\"),\n            version=cls._find_single_string(setup_call, body, \"version\") or \"0.0.0\",\n            install_requires=cls._find_install_requires(setup_call, body),\n            extras_require=cls._find_extras_require(setup_call, body),\n            python_requires=cls._find_single_string(setup_call, body, \"python_requires\"),\n        )\n\n    @staticmethod\n    def read_setup_cfg(file: Path) -> Setup:\n        parser = ConfigParser()\n\n        parser.read(str(file))\n\n        name = None\n        version = \"0.0.0\"\n        if parser.has_option(\"metadata\", \"name\"):\n            name = parser.get(\"metadata\", \"name\")\n\n        if parser.has_option(\"metadata\", \"version\"):\n            meta_version = parser.get(\"metadata\", \"version\")\n            if not meta_version.startswith(\"attr:\"):\n                version = meta_version\n\n        install_requires = []\n        extras_require: dict[str, list[str]] = {}\n        python_requires = None\n        if parser.has_section(\"options\"):\n            if parser.has_option(\"options\", \"install_requires\"):\n                for dep in parser.get(\"options\", \"install_requires\").split(\"\\n\"):\n                    dep = dep.strip()\n                    if not dep:\n                        continue\n\n                    install_requires.append(dep)\n\n            if parser.has_option(\"options\", \"python_requires\"):\n                python_requires = parser.get(\"options\", \"python_requires\")\n\n        if parser.has_section(\"options.extras_require\"):\n            for group in parser.options(\"options.extras_require\"):\n                extras_require[group] = []\n                deps = parser.get(\"options.extras_require\", group)\n                for dep in deps.split(\"\\n\"):\n                    dep = dep.strip()\n                    if not dep:\n                        continue\n\n                    extras_require[group].append(dep)\n\n        return Setup(\n            name=name,\n            version=version,\n            install_requires=install_requires,\n            extras_require=extras_require,\n            python_requires=python_requires,\n        )\n\n    @classmethod\n    def _find_setup_call(cls, elements: list[Any]) -> tuple[ast.Call | None, list[Any | None]]:\n        funcdefs = []\n        for i, element in enumerate(elements):\n            if isinstance(element, ast.If) and i == len(elements) - 1:\n                # Checking if the last element is an if statement\n                # and if it is 'if __name__ == \"__main__\"' which\n                # could contain the call to setup()\n                test = element.test\n                if not isinstance(test, ast.Compare):\n                    continue\n\n                left = test.left\n                if not isinstance(left, ast.Name):\n                    continue\n\n                if left.id != \"__name__\":\n                    continue\n\n                setup_call, body = cls._find_sub_setup_call([element])\n                if not setup_call:\n                    continue\n\n                return setup_call, body + elements\n            if not isinstance(element, ast.Expr):\n                if isinstance(element, ast.FunctionDef):\n                    funcdefs.append(element)\n\n                continue\n\n            value = element.value\n            if not isinstance(value, ast.Call):\n                continue\n\n            func = value.func\n            if not (isinstance(func, ast.Name) and func.id == \"setup\") and not (\n                isinstance(func, ast.Attribute)\n                and isinstance(func.value, ast.Name)\n                and func.value.id == \"setuptools\"\n                and func.attr == \"setup\"\n            ):\n                continue\n\n            return value, elements\n\n        # Nothing, we inspect the function definitions\n        return cls._find_sub_setup_call(funcdefs)\n\n    @no_type_check\n    @classmethod\n    def _find_sub_setup_call(cls, elements: list[Any]) -> tuple[ast.Call | None, list[Any | None]]:\n        for element in elements:\n            if not isinstance(element, (ast.FunctionDef, ast.If)):\n                continue\n\n            setup_call = cls._find_setup_call(element.body)\n            if setup_call != (None, None):\n                setup_call, body = setup_call\n\n                body = elements + body\n\n                return setup_call, body\n\n        return None, None\n\n    @no_type_check\n    @classmethod\n    def _find_install_requires(cls, call: ast.Call, body: Iterable[Any]) -> list[str]:\n        install_requires: list[str] = []\n        value = cls._find_in_call(call, \"install_requires\")\n        if value is None:\n            # Trying to find in kwargs\n            kwargs = cls._find_call_kwargs(call)\n\n            if kwargs is None or not isinstance(kwargs, ast.Name):\n                return install_requires\n\n            variable = cls._find_variable_in_body(body, kwargs.id)\n            if not isinstance(variable, (ast.Dict, ast.Call)):\n                return install_requires\n\n            if isinstance(variable, ast.Call):\n                if not isinstance(variable.func, ast.Name):\n                    return install_requires\n\n                if variable.func.id != \"dict\":\n                    return install_requires\n\n                value = cls._find_in_call(variable, \"install_requires\")\n            else:\n                value = cls._find_in_dict(variable, \"install_requires\")\n\n        if value is None:\n            return install_requires\n\n        if isinstance(value, ast.List):\n            install_requires.extend(\n                [el.value for el in value.elts if isinstance(el, ast.Constant) and isinstance(el.value, str)]\n            )\n        elif isinstance(value, ast.Name):\n            variable = cls._find_variable_in_body(body, value.id)\n\n            if variable is not None and isinstance(variable, ast.List):\n                install_requires.extend(\n                    [el.value for el in variable.elts if isinstance(el, ast.Constant) and isinstance(el.value, str)]\n                )\n\n        return install_requires\n\n    @no_type_check\n    @classmethod\n    def _find_extras_require(cls, call: ast.Call, body: Iterable[Any]) -> dict[str, list[str]]:\n        extras_require: dict[str, list[str]] = {}\n        value = cls._find_in_call(call, \"extras_require\")\n        if value is None:\n            # Trying to find in kwargs\n            kwargs = cls._find_call_kwargs(call)\n\n            if kwargs is None or not isinstance(kwargs, ast.Name):\n                return extras_require\n\n            variable = cls._find_variable_in_body(body, kwargs.id)\n            if not isinstance(variable, (ast.Dict, ast.Call)):\n                return extras_require\n\n            if isinstance(variable, ast.Call):\n                if not isinstance(variable.func, ast.Name):\n                    return extras_require\n\n                if variable.func.id != \"dict\":\n                    return extras_require\n\n                value = cls._find_in_call(variable, \"extras_require\")\n            else:\n                value = cls._find_in_dict(variable, \"extras_require\")\n\n        if value is None:\n            return extras_require\n\n        if isinstance(value, ast.Dict):\n            for key, val in zip(value.keys, value.values):\n                if isinstance(val, ast.Name):\n                    val = cls._find_variable_in_body(body, val.id)\n\n                if isinstance(val, ast.List):\n                    extras_require[key.value] = [\n                        e.value for e in val.elts if isinstance(e, ast.Constant) and isinstance(e.value, str)\n                    ]\n        elif isinstance(value, ast.Name):\n            variable = cls._find_variable_in_body(body, value.id)\n\n            if variable is None or not isinstance(variable, ast.Dict):\n                return extras_require\n\n            for key, val in zip(variable.keys, variable.values):\n                if isinstance(val, ast.Name):\n                    val = cls._find_variable_in_body(body, val.id)\n\n                if isinstance(val, ast.List):\n                    extras_require[key.value] = [\n                        e.value for e in val.elts if isinstance(e, ast.Constant) and isinstance(e.value, str)\n                    ]\n\n        return extras_require\n\n    @classmethod\n    def _find_single_string(cls, call: ast.Call, body: list[Any], name: str) -> str | None:\n        value = cls._find_in_call(call, name)\n        if value is None:\n            # Trying to find in kwargs\n            kwargs = cls._find_call_kwargs(call)\n\n            if kwargs is None or not isinstance(kwargs, ast.Name):\n                return None\n\n            variable = cls._find_variable_in_body(body, kwargs.id)\n            if not isinstance(variable, (ast.Dict, ast.Call)):\n                return None\n\n            if isinstance(variable, ast.Call):\n                if not isinstance(variable.func, ast.Name):\n                    return None\n\n                if variable.func.id != \"dict\":\n                    return None\n\n                value = cls._find_in_call(variable, name)\n            else:\n                value = cls._find_in_dict(variable, name)\n\n        if value is None:\n            return None\n\n        if isinstance(value, ast.Constant) and isinstance(value.value, str):\n            return value.value\n        elif isinstance(value, ast.Name):\n            variable = cls._find_variable_in_body(body, value.id)\n\n            if variable is not None and isinstance(variable, ast.Constant) and isinstance(variable.value, str):\n                return variable.value\n\n        return None\n\n    @staticmethod\n    def _find_in_call(call: ast.Call, name: str) -> Any | None:\n        for keyword in call.keywords:\n            if keyword.arg == name:\n                return keyword.value\n        return None\n\n    @staticmethod\n    def _find_call_kwargs(call: ast.Call) -> Any | None:\n        kwargs = None\n        for keyword in call.keywords:\n            if keyword.arg is None:\n                kwargs = keyword.value\n\n        return kwargs\n\n    @staticmethod\n    def _find_variable_in_body(body: Iterable[Any], name: str) -> Any | None:\n        for elem in body:\n            if not isinstance(elem, ast.Assign):\n                continue\n\n            for target in elem.targets:\n                if not isinstance(target, ast.Name):\n                    continue\n\n                if target.id == name:\n                    return elem.value\n        return None\n\n    @staticmethod\n    def _find_in_dict(dict_: ast.Dict, name: str) -> Any | None:\n        for key, val in zip(dict_.keys, dict_.values):\n            if isinstance(key, ast.Constant) and key.value == name:\n                return val\n        return None\n\n\nclass SetupDistribution(Distribution):\n    def __init__(self, data: Setup) -> None:\n        self._data = data\n\n    def read_text(self, filename: str) -> str | None:\n        return None\n\n    def locate_file(self, path: str | os.PathLike[str]) -> _SimplePath:\n        return Path()\n\n    @property\n    def metadata(self) -> dict[str, Any]:  # type: ignore[override]\n        return {\n            k: v\n            for k, v in {\n                \"Name\": self._data.name,\n                \"Version\": self._data.version,\n                \"Summary\": self._data.summary,\n                \"Requires-Python\": self._data.python_requires,\n            }.items()\n            if v is not None\n        }\n\n    @property\n    def requires(self) -> list[str] | None:\n        from pdm.models.markers import get_marker\n        from pdm.models.requirements import parse_requirement\n\n        result = self._data.install_requires[:]\n        for extra, reqs in self._data.extras_require.items():\n            extra_marker = f\"extra == '{extra}'\"\n            for req in reqs:\n                parsed = parse_requirement(req)\n                old_marker = str(parsed.marker) if parsed.marker else None\n                if old_marker:\n                    if \" or \" in old_marker:\n                        new_marker = f\"({old_marker}) and {extra_marker}\"\n                    else:\n                        new_marker = f\"{old_marker} and {extra_marker}\"\n                else:\n                    new_marker = extra_marker\n                parsed.marker = get_marker(new_marker)\n                result.append(parsed.as_line())\n        return result\n"
  },
  {
    "path": "src/pdm/models/specifiers.py",
    "content": "from __future__ import annotations\n\nimport dataclasses\nimport itertools\nimport json\nimport re\nimport warnings\nfrom functools import lru_cache\nfrom operator import attrgetter\nfrom typing import Any, Iterable, Match, cast\n\nfrom dep_logic.specifiers import (\n    BaseSpecifier,\n    EmptySpecifier,\n    RangeSpecifier,\n    UnionSpecifier,\n    VersionSpecifier,\n    from_specifierset,\n)\nfrom packaging.specifiers import SpecifierSet\n\nfrom pdm.exceptions import InvalidPyVersion\nfrom pdm.models.versions import Version\nfrom pdm.utils import parse_version\n\n\ndef _read_max_versions() -> dict[Version, int]:\n    from pdm.compat import resources_open_binary\n\n    with resources_open_binary(\"pdm.models\", \"python_max_versions.json\") as fp:\n        return {Version(k): v for k, v in json.load(fp).items()}\n\n\n@lru_cache\ndef get_specifier(version_str: str | None) -> SpecifierSet:\n    if not version_str or version_str == \"*\":\n        return SpecifierSet()\n    return SpecifierSet(version_str)\n\n\n_legacy_specifier_re = re.compile(r\"(==|!=|<=|>=|<|>)(\\s*)([^,;\\s)]*)\")\n\n\n@lru_cache\ndef fix_legacy_specifier(specifier: str) -> str:\n    \"\"\"Since packaging 22.0, legacy specifiers like '>=4.*' are no longer\n    supported. We try to normalize them to the new format.\n    \"\"\"\n\n    def fix_wildcard(match: Match[str]) -> str:\n        operator, _, version = match.groups()\n        if operator in (\"==\", \"!=\"):\n            return match.group(0)\n        if \".*\" in version:\n            warnings.warn(\".* suffix can only be used with `==` or `!=` operators\", FutureWarning, stacklevel=4)\n            version = version.replace(\".*\", \".0\")\n            if operator in (\"<\", \"<=\"):  # <4.* and <=4.* are equivalent to <4.0\n                operator = \"<\"\n            elif operator in (\">\", \">=\"):  # >4.* and >=4.* are equivalent to >=4.0\n                operator = \">=\"\n        elif \"+\" in version:  # Drop the local version\n            warnings.warn(\n                \"Local version label can only be used with `==` or `!=` operators\", FutureWarning, stacklevel=4\n            )\n            version = version.split(\"+\")[0]\n        return f\"{operator}{version}\"\n\n    return _legacy_specifier_re.sub(fix_wildcard, specifier)\n\n\nclass PySpecSet(SpecifierSet):\n    \"\"\"A custom SpecifierSet that supports merging with logic operators (&, |).\"\"\"\n\n    PY_MAX_MINOR_VERSION = _read_max_versions()\n    MAX_MAJOR_VERSION = max(PY_MAX_MINOR_VERSION)[:1].bump()\n\n    __slots__ = (\"_logic\", \"_prereleases\", \"_specs\")\n\n    def __init__(self, spec: str | VersionSpecifier = \"\") -> None:\n        if spec == \"<empty>\":\n            spec = EmptySpecifier()\n        if isinstance(spec, BaseSpecifier):\n            super().__init__(self._normalize(spec))\n            self._logic = spec\n            return\n        try:\n            if spec == \"*\":  # pragma: no cover\n                spec = \"\"\n            super().__init__(fix_legacy_specifier(spec))\n            self._logic = from_specifierset(self)\n        except ValueError:\n            raise InvalidPyVersion(f\"Invalid specifier: {spec}\") from None\n\n    def __hash__(self) -> int:\n        return hash(self._logic)\n\n    def __str__(self) -> str:\n        if self.is_empty():\n            return \"<empty>\"\n        return super().__str__()\n\n    def __eq__(self, other: Any) -> bool:\n        if not isinstance(other, PySpecSet):\n            return NotImplemented\n        return self._logic == other._logic\n\n    def is_empty(self) -> bool:\n        \"\"\"Check whether the specifierset contains any valid versions.\"\"\"\n        return self._logic.is_empty()\n\n    def is_any(self) -> bool:\n        \"\"\"Return True if the specifierset accepts all versions.\"\"\"\n        return self._logic.is_any()\n\n    @classmethod\n    def _normalize(cls, spec: VersionSpecifier) -> str:\n        if spec.is_empty():\n            return \"\"\n        if not isinstance(spec, UnionSpecifier):\n            return str(spec)\n        ranges, next_ranges = itertools.tee(sorted(spec.ranges))\n        next(next_ranges, None)\n        whole_range = RangeSpecifier(\n            min=spec.ranges[0].min,\n            max=spec.ranges[-1].max,\n            include_min=spec.ranges[0].include_min,\n            include_max=spec.ranges[-1].include_max,\n        )\n        parts = [] if whole_range.is_any() else [str(whole_range)]\n        for left, right in zip(ranges, next_ranges):\n            assert left.max is not None and right.min is not None\n            start = Version(left.max.release).complete()\n            end = Version(right.min.release).complete()\n            if left.include_max:\n                start = start.bump()\n            if not right.include_min:\n                end = end.bump()\n            parts.extend(f\"!={v}\" for v in cls._populate_version_range(start, end))\n        return \",\".join(parts)\n\n    def __repr__(self) -> str:\n        return f\"<PySpecSet {self}>\"\n\n    def __and__(self, other: Any) -> PySpecSet:\n        if isinstance(other, PySpecSet):\n            return type(self)(self._logic & other._logic)\n        elif isinstance(other, VersionSpecifier):\n            return type(self)(self._logic & other)\n        return NotImplemented\n\n    def __or__(self, other: Any) -> PySpecSet:\n        if isinstance(other, PySpecSet):\n            return type(self)(self._logic | other._logic)\n        elif isinstance(other, VersionSpecifier):\n            return type(self)(self._logic | other)\n        return NotImplemented\n\n    @classmethod\n    def _populate_version_range(cls, lower: Version, upper: Version) -> Iterable[Version]:\n        \"\"\"Expand the version range to a collection of versions to exclude,\n        taking the released python versions into consideration.\n        \"\"\"\n        assert lower < upper\n        prev = lower\n        while prev < upper:\n            if prev[-2:] == Version((0, 0)):  # X.0.0\n                cur = prev.bump(0)  # X+1.0.0\n                if cur <= upper:  # It is still within the range\n                    yield Version((prev[0], \"*\"))  # Exclude the whole major series: X.*\n                    prev = cur\n                    continue\n            if prev[-1] == 0:  # X.Y.0\n                cur = prev.bump(1)  # X.Y+1.0\n                if cur <= upper:  # It is still within the range\n                    yield prev[:2].complete(\"*\")  # Exclude X.Y.*\n                    prev = (\n                        prev.bump(0) if cur.is_py2 and cast(int, cur[1]) > cls.PY_MAX_MINOR_VERSION[cur[:1]] else cur\n                    )  # If prev is 2.7, next is 3.0, otherwise next is X.Y+1.0\n                    continue\n                while prev < upper:\n                    # Iterate each version from X.Y.0(prev) to X.Y.Z(upper)\n                    yield prev\n                    prev = prev.bump()\n                break\n            # Can't produce any wildcard versions\n            cur = prev.bump(1)\n            if cur <= upper:  # X.Y+1.0 is still within the range\n                current_max = cls.PY_MAX_MINOR_VERSION[prev[:2]]\n                for z in range(cast(int, prev[2]), current_max + 1):\n                    yield prev[:2].complete(z)\n                prev = prev.bump(0) if cur.is_py2 and cast(int, cur[1]) > cls.PY_MAX_MINOR_VERSION[cur[:1]] else cur\n            else:  # Produce each version from X.Y.Z to X.Y.W\n                while prev < upper:\n                    yield prev\n                    prev = prev.bump()\n                break\n\n    @lru_cache\n    def is_superset(self, other: str | PySpecSet) -> bool:\n        if self.is_empty():\n            return False\n        this = _fix_py4k(self._logic)\n        if this.is_any():\n            return True\n        if isinstance(other, str):\n            other = type(self)(other)\n        return this & other._logic == other._logic\n\n    @lru_cache\n    def is_subset(self, other: str | PySpecSet) -> bool:\n        if self.is_empty():\n            return False\n        if isinstance(other, str):\n            other = type(self)(other)\n        that = _fix_py4k(other._logic)\n        if that.is_any():\n            return True\n        return self._logic & that == self._logic\n\n    def as_marker_string(self) -> str:\n        spec = self._logic\n        if spec.is_empty():\n            raise InvalidPyVersion(\"Impossible specifier\")\n        if spec.is_any():\n            return \"\"\n        return _convert_spec(cast(VersionSpecifier, spec))\n\n\ndef _convert_spec(specifier: VersionSpecifier) -> str:\n    if isinstance(specifier, UnionSpecifier):\n        return \" or \".join(_convert_spec(s) for s in specifier.ranges)\n\n    result: list[str] = []\n    excludes: list[str] = []\n    full_excludes: list[str] = []\n    for spec in sorted(specifier.to_specifierset(), key=attrgetter(\"version\")):\n        op, version = spec.operator, spec.version\n        if len(version.split(\".\")) < 3:\n            key = \"python_version\"\n        else:\n            key = \"python_full_version\"\n            if version[-2:] == \".*\":\n                version = version[:-2]\n                key = \"python_version\"\n        if op == \"!=\":\n            if key == \"python_version\":\n                excludes.append(version)\n            else:\n                full_excludes.append(version)\n        else:\n            result.append(f\"{key}{op}{version!r}\")\n    if excludes:\n        result.append(\"python_version not in {!r}\".format(\", \".join(sorted(excludes))))\n    if full_excludes:\n        result.append(\"python_full_version not in {!r}\".format(\", \".join(sorted(full_excludes))))\n    return \" and \".join(result)\n\n\ndef _fix_py4k(spec: VersionSpecifier) -> VersionSpecifier:\n    \"\"\"If the upper bound is 4.0, replace it with unlimited.\"\"\"\n    if isinstance(spec, UnionSpecifier):\n        *pre, last = spec.ranges\n        return UnionSpecifier([*pre, _fix_py4k(last)])\n    if isinstance(spec, RangeSpecifier) and spec.max == parse_version(\"4.0\"):\n        return dataclasses.replace(spec, max=None, include_max=False)\n    return spec\n"
  },
  {
    "path": "src/pdm/models/venv.py",
    "content": "from __future__ import annotations\n\nimport dataclasses as dc\nimport sys\nfrom functools import cached_property\nfrom pathlib import Path\n\nfrom pdm.models.in_process import get_sys_config_paths\nfrom pdm.utils import find_python_in_path, get_venv_like_prefix\n\nIS_WIN = sys.platform == \"win32\"\nBIN_DIR = \"Scripts\" if IS_WIN else \"bin\"\n\n\ndef get_venv_python(venv: Path) -> Path:\n    \"\"\"Get the interpreter path inside the given venv.\"\"\"\n    suffix = \".exe\" if IS_WIN else \"\"\n    result = venv / BIN_DIR / f\"python{suffix}\"\n    if IS_WIN and not result.exists():\n        result = venv / \"bin\" / f\"python{suffix}\"  # for mingw64/msys2\n        if result.exists():\n            return result\n        else:\n            return venv / \"python.exe\"  # for conda\n    return result\n\n\ndef is_conda_venv(root: Path) -> bool:\n    return (root / \"conda-meta\").exists()\n\n\n@dc.dataclass(frozen=True)\nclass VirtualEnv:\n    root: Path\n    is_conda: bool\n    interpreter: Path\n\n    @classmethod\n    def get(cls, root: Path) -> VirtualEnv | None:\n        path = get_venv_python(root)\n        if not path.exists():\n            return None\n        return cls(root, is_conda_venv(root), path)\n\n    @classmethod\n    def from_interpreter(cls, interpreter: Path) -> VirtualEnv | None:\n        root, is_conda = get_venv_like_prefix(interpreter)\n        if root is not None:\n            return cls(root, is_conda, interpreter)\n        return None\n\n    def env_vars(self) -> dict[str, str]:\n        key = \"CONDA_PREFIX\" if self.is_conda else \"VIRTUAL_ENV\"\n        return {key: str(self.root)}\n\n    @cached_property\n    def venv_config(self) -> dict[str, str]:\n        venv_cfg = self.root / \"pyvenv.cfg\"\n        if not venv_cfg.exists():\n            return {}\n        parsed: dict[str, str] = {}\n        with venv_cfg.open(encoding=\"utf-8\") as fp:\n            for line in fp:\n                if \"=\" in line:\n                    k, v = line.split(\"=\", 1)\n                    k = k.strip().lower()\n                    v = v.strip()\n                    if k == \"include-system-site-packages\":\n                        v = v.lower()\n                    parsed[k] = v\n        return parsed\n\n    @property\n    def include_system_site_packages(self) -> bool:\n        return self.venv_config.get(\"include-system-site-packages\") == \"true\"\n\n    @cached_property\n    def base_paths(self) -> list[str]:\n        home = Path(self.venv_config[\"home\"])\n        base_executable = find_python_in_path(home) or find_python_in_path(home.parent)\n        assert base_executable is not None\n        paths = get_sys_config_paths(str(base_executable))\n        return [paths[\"purelib\"], paths[\"platlib\"]]\n"
  },
  {
    "path": "src/pdm/models/versions.py",
    "content": "from __future__ import annotations\n\nimport re\nfrom typing import TYPE_CHECKING, overload\n\nfrom pdm.exceptions import InvalidPyVersion\n\nif TYPE_CHECKING:\n    from typing import Any, Literal, Union\n\n    VersionBit = Union[int, Literal[\"*\"]]\n\nPRE_RELEASE_SEGMENT_RE = re.compile(\n    r\"(?P<digit>\\d+)(?P<type>a|b|rc)(?P<n>\\d*)\",\n    flags=re.IGNORECASE,\n)\n\n\nclass Version:\n    \"\"\"A loosely semantic version implementation that allows '*' in version part.\n\n    This class is designed for Python specifier set merging only, hence up to 3 version\n    parts are kept, plus optional prerelease suffix.\n\n    This is a slightly different purpose than packaging.version.Version which is\n    focused on supporting PEP 440 version identifiers, not specifiers.\n    \"\"\"\n\n    MIN: Version\n    MAX: Version\n    # Pre-release may follow version with {a|b|rc}N\n    # https://docs.python.org/3/faq/general.html#how-does-the-python-version-numbering-scheme-work\n    pre: tuple[str, int] | None = None\n\n    def __init__(self, version: tuple[VersionBit, ...] | str) -> None:\n        if isinstance(version, str):\n            version_str = re.sub(r\"(?<!\\.)\\*\", \".*\", version)\n            bits: list[VersionBit] = []\n            for v in version_str.split(\".\")[:3]:\n                try:\n                    bits.append(int(v))\n                except ValueError:\n                    pre_m = PRE_RELEASE_SEGMENT_RE.match(v)\n                    if v == \"*\":\n                        bits.append(\"*\")\n                        break  # .* is only allowed at the end, per PEP 440\n                    elif pre_m:\n                        bits.append(int(pre_m.group(\"digit\")))\n                        pre_type = pre_m.group(\"type\").lower()\n                        pre_n = int(pre_m.group(\"n\") or \"0\")\n                        self.pre = (pre_type, pre_n)\n                        break  # pre release version is only at the end\n                    else:\n                        raise InvalidPyVersion(\n                            f\"{version_str}: postreleases are not supported for python version specifiers.\"\n                        ) from None\n            version = tuple(bits)\n        self._version: tuple[VersionBit, ...] = version\n\n    def complete(self, complete_with: VersionBit = 0, max_bits: int = 3) -> Version:\n        \"\"\"\n        Complete the version with the given bit if the version has less than max parts\n        \"\"\"\n        assert len(self._version) <= max_bits, self\n        new_tuple = self._version + (max_bits - len(self._version)) * (complete_with,)\n        ret = type(self)(new_tuple)\n        ret.pre = self.pre\n        return ret\n\n    def bump(self, idx: int = -1) -> Version:\n        \"\"\"Bump version by incrementing 1 on the given index of version part.\n        If index is not provided: increment the last version bit unless version\n        is a pre-release, in which case, increment the pre-release number.\n        \"\"\"\n        version = self._version\n        if idx == -1 and self.pre:\n            ret = type(self)(version).complete()\n            ret.pre = (self.pre[0], self.pre[1] + 1)\n        else:\n            head, value = version[:idx], int(version[idx])\n            ret = type(self)((*head, value + 1)).complete()\n            ret.pre = None\n        return ret\n\n    def startswith(self, other: Version) -> bool:\n        \"\"\"Check if the version begins with another version.\"\"\"\n        return self._version[: len(other._version)] == other._version\n\n    @property\n    def is_wildcard(self) -> bool:\n        \"\"\"Check if the version ends with a '*'\"\"\"\n        return self._version[-1] == \"*\"\n\n    @property\n    def is_prerelease(self) -> bool:\n        \"\"\"Check if the version is a prerelease.\"\"\"\n        return self.pre is not None\n\n    def __str__(self) -> str:\n        parts = []\n        parts.append(\".\".join(map(str, self._version)))\n\n        if self.pre:\n            parts.append(\"\".join(str(x) for x in self.pre))\n\n        return \"\".join(parts)\n\n    def __repr__(self) -> str:\n        return f\"<Version({self})>\"\n\n    def __eq__(self, other: Any) -> bool:\n        if not isinstance(other, Version):\n            return NotImplemented\n        return self._version == other._version and self.pre == other.pre\n\n    def __lt__(self, other: Any) -> bool:\n        if not isinstance(other, Version):\n            return NotImplemented\n\n        def comp_key(version: Version) -> list[float]:\n            ret: list[float] = [-1 if v == \"*\" else v for v in version._version]\n            if version.pre:\n                # Get the ascii value of first character, a < b < r[c]\n                ret += [ord(version.pre[0][0]), version.pre[1]]\n            else:\n                ret += [float(\"inf\")]\n\n            return ret\n\n        return comp_key(self) < comp_key(other)\n\n    def __gt__(self, other: Any) -> bool:\n        return not (self.__lt__(other) or self.__eq__(other))\n\n    def __le__(self, other: Any) -> bool:\n        return self.__lt__(other) or self.__eq__(other)\n\n    def __ge__(self, other: Any) -> bool:\n        return self.__gt__(other) or self.__eq__(other)\n\n    @overload\n    def __getitem__(self, idx: int) -> VersionBit: ...\n\n    @overload\n    def __getitem__(self, idx: slice) -> Version: ...\n\n    def __getitem__(self, idx: int | slice) -> VersionBit | Version:\n        if isinstance(idx, slice):\n            return type(self)(self._version[idx])\n        else:\n            return self._version[idx]\n\n    def __setitem__(self, idx: int, value: VersionBit) -> None:\n        if not isinstance(idx, int):\n            raise TypeError(\"Slice assignment is not supported\")\n        version = list(self._version)\n        version[idx] = value\n        self._version = tuple(version)\n\n    def __hash__(self) -> int:\n        return hash((self._version, self.pre))\n\n    @property\n    def is_py2(self) -> bool:\n        return self._version[0] == 2\n\n\nVersion.MIN = Version((-1, -1, -1))\nVersion.MAX = Version((99, 99, 99))\n"
  },
  {
    "path": "src/pdm/models/working_set.py",
    "content": "from __future__ import annotations\n\nimport itertools\nimport sys\nfrom collections import ChainMap\nfrom pathlib import Path\nfrom typing import Iterable, Iterator, Mapping\n\nfrom pdm.compat import importlib_metadata as im\nfrom pdm.utils import normalize_name\n\ndefault_context = im.DistributionFinder.Context()\n\n\nclass EgglinkFinder(im.DistributionFinder):\n    @classmethod\n    def find_distributions(cls, context: im.DistributionFinder.Context = default_context) -> Iterable[im.Distribution]:\n        found_links = cls._search_paths(context.name, context.path)\n        # For Py3.7 compatibility, handle both classmethod and instance method\n        meta_finder = im.MetadataPathFinder()\n        for link in found_links:\n            name = link.stem\n            with link.open(\"rb\") as file_link:\n                link_pointer = Path(file_link.readline().decode().strip())\n            dist = next(\n                iter(\n                    meta_finder.find_distributions(im.DistributionFinder.Context(name=name, path=[str(link_pointer)]))\n                ),\n                None,\n            )\n            if not dist:\n                continue\n            dist.link_file = link.absolute()  # type: ignore[attr-defined]\n            yield dist\n\n    @classmethod\n    def _search_paths(cls, name: str | None, paths: list[str]) -> Iterable[Path]:\n        for path in paths:\n            if name:\n                if Path(path).joinpath(f\"{name}.egg-link\").is_file():\n                    yield Path(path).joinpath(f\"{name}.egg-link\")\n            else:\n                yield from Path(path).glob(\"*.egg-link\")\n\n\ndef distributions(path: list[str]) -> Iterable[im.Distribution]:\n    \"\"\"Find distributions in the paths. Similar to `importlib.metadata`'s\n    implementation but with the ability to discover egg-links.\n    \"\"\"\n    context = im.DistributionFinder.Context(path=path)\n    resolvers = itertools.chain(\n        filter(\n            None,\n            (getattr(finder, \"find_distributions\", None) for finder in sys.meta_path),\n        ),\n        (EgglinkFinder.find_distributions,),\n    )\n    return itertools.chain.from_iterable(resolver(context) for resolver in resolvers)\n\n\nclass WorkingSet(Mapping[str, im.Distribution]):\n    \"\"\"A dictionary of currently installed distributions\"\"\"\n\n    def __init__(self, paths: list[str] | None = None, shared_paths: list[str] | None = None) -> None:\n        if paths is None:\n            paths = sys.path\n        if shared_paths is None:\n            shared_paths = []\n        self._dist_map = {\n            normalize_name(dist.metadata[\"Name\"]): dist\n            for dist in distributions(path=list(dict.fromkeys(paths)))\n            if dist.metadata.get(\"Name\")\n        }\n        self._shared_map = {\n            normalize_name(dist.metadata[\"Name\"]): dist\n            for dist in distributions(path=list(dict.fromkeys(shared_paths)))\n            if dist.metadata.get(\"Name\")\n        }\n        self._iter_map = ChainMap(self._dist_map, self._shared_map)\n\n    def __getitem__(self, key: str) -> im.Distribution:\n        return self._iter_map[key]\n\n    def is_owned(self, key: str) -> bool:\n        return key in self._dist_map\n\n    def __len__(self) -> int:\n        return len(self._iter_map)\n\n    def __iter__(self) -> Iterator[str]:\n        return iter(self._iter_map)\n\n    def __repr__(self) -> str:\n        return repr(self._iter_map)\n"
  },
  {
    "path": "src/pdm/pep582/__init__.py",
    "content": ""
  },
  {
    "path": "src/pdm/pep582/sitecustomize.py",
    "content": "import os\nimport site\nimport sys\nfrom pathlib import Path\n\n\ndef get_pypackages_path():\n    def find_pypackage(path, version):\n        if not path.exists():\n            return None\n\n        packages_name = f\"__pypackages__/{version}/lib\"\n        files = list(path.glob(packages_name))\n        if files:\n            return str(files[0])\n\n        if path == path.parent:\n            return None\n\n        return find_pypackage(path.parent, version)\n\n    if \"PEP582_PACKAGES\" in os.environ:\n        return os.path.join(os.getenv(\"PEP582_PACKAGES\"), \"lib\")\n    find_paths = [os.getcwd()]\n    version = bare_version = \".\".join(map(str, sys.version_info[:2]))\n    if os.name == \"nt\" and sys.maxsize <= 2**32:\n        version += \"-32\"\n\n    if getattr(sys, \"argv\", None) and sys.argv[0]:\n        script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))\n        find_paths.insert(0, script_dir)\n\n    for path in find_paths:\n        result = find_pypackage(Path(path), version)\n        if result:\n            return result\n\n    if bare_version != version:\n        for path in find_paths:\n            result = find_pypackage(Path(path), bare_version)\n            if result:\n                return result\n\n\ndef load_next_sitecustomize_py2():\n    import imp\n\n    try:\n        f, pathname, desc = imp.find_module(\"sitecustomize\", sys.path)\n        try:\n            imp.load_module(\"another_sitecustomize\", f, pathname, desc)\n        finally:\n            f.close()\n    except ImportError:\n        pass\n\n\ndef load_next_sitecustomize_py3():\n    import importlib.util\n\n    old_module = sys.modules.pop(\"sitecustomize\", None)\n    spec = importlib.util.find_spec(\"sitecustomize\")\n    if spec is not None:\n        module = importlib.util.module_from_spec(spec)\n        spec.loader.exec_module(module)\n    if old_module is not None:\n        sys.modules[\"sitecustomize\"] = old_module\n\n\ndef patch_sysconfig(libpath):\n    \"\"\"This is a hack to make sure that the sysconfig.get_paths()\n    returns PEP 582 scheme.\n    \"\"\"\n    import functools\n    import sysconfig\n\n    bin_prefix = \"Scripts\" if os.name == \"nt\" else \"bin\"\n    pep582_base = os.path.dirname(libpath)\n    pep582_scheme = {\n        \"stdlib\": \"{pep582_base}/lib\",\n        \"platstdlib\": \"{pep582_base}/lib\",\n        \"purelib\": \"{pep582_base}/lib\",\n        \"platlib\": \"{pep582_base}/lib\",\n        \"include\": \"{pep582_base}/include\",\n        \"scripts\": f\"{{pep582_base}}/{bin_prefix}\",\n        \"data\": \"{pep582_base}\",\n        \"prefix\": \"{pep582_base}\",\n        \"headers\": \"{pep582_base}/include\",\n    }\n\n    def patch_pep582(get_paths):\n        @functools.wraps(get_paths)\n        def wrapper(scheme=None, vars=None, expand=True):\n            default_scheme = get_paths.__defaults__[0]\n            if not vars and scheme is None:\n                scheme = \"pep582\"\n            else:\n                scheme = scheme or default_scheme\n            return get_paths(scheme, vars, expand)\n\n        return wrapper\n\n    # This returns a global variable, just update it in place.\n    sysconfig.get_config_vars()[\"pep582_base\"] = pep582_base\n    sysconfig.get_paths = patch_pep582(sysconfig.get_paths)\n    sysconfig._INSTALL_SCHEMES[\"pep582\"] = pep582_scheme\n\n\ndef main():\n    self_path = os.path.normcase(os.path.dirname(os.path.abspath(__file__)))\n    sys.path[:] = [path for path in sys.path if os.path.normcase(path) != self_path]\n\n    if sys.version_info[0] == 2:  # noqa: UP036\n        load_next_sitecustomize_py2()\n    else:\n        load_next_sitecustomize_py3()\n\n    libpath = get_pypackages_path()\n    if not libpath:\n        return\n\n    # First, drop site related paths.\n    original_sys_path = sys.path[:]\n    known_paths = set()\n    site.addusersitepackages(known_paths)\n    site.addsitepackages(known_paths)\n    known_paths = {os.path.normcase(path) for path in known_paths}\n    original_sys_path = [path for path in original_sys_path if os.path.normcase(path) not in known_paths]\n    sys.path[:] = original_sys_path\n\n    # Second, add lib directories, ensuring .pth file are processed.\n    site.addsitedir(libpath)\n    if not os.environ.pop(\"NO_SITE_PACKAGES\", None):\n        # Then add the removed path to the tail of the paths\n        known_paths.clear()\n        site.addusersitepackages(known_paths)\n        site.addsitepackages(known_paths)\n    if \"PEP582_PACKAGES\" in os.environ:\n        patch_sysconfig(libpath)\n\n\nmain()\ndel main\n"
  },
  {
    "path": "src/pdm/project/__init__.py",
    "content": "from pdm.project.config import Config, ConfigItem\nfrom pdm.project.core import Project\n\n__all__ = [\"Config\", \"ConfigItem\", \"Project\"]\n"
  },
  {
    "path": "src/pdm/project/config.py",
    "content": "from __future__ import annotations\n\nimport collections\nimport dataclasses\nimport os\nfrom functools import cached_property\nfrom pathlib import Path\nfrom typing import Any, Callable, ClassVar, Iterator, Mapping, MutableMapping, cast\n\nimport platformdirs\nimport rich.theme\n\nfrom pdm import termui\nfrom pdm._types import RepositoryConfig\nfrom pdm.compat import tomllib\nfrom pdm.exceptions import NoConfigError, PdmUsageError\n\nREPOSITORY = \"repository\"\nSOURCE = \"pypi\"\nDEFAULT_REPOSITORIES = {\n    \"pypi\": \"https://upload.pypi.org/legacy/\",\n    \"testpypi\": \"https://test.pypi.org/legacy/\",\n}\n\nui = termui.UI()\n\n\ndef choices(*choices: str) -> Callable[[str], str]:\n    def converter(value: str) -> str:\n        if value not in choices:\n            raise ValueError(f\"Value must be one of {choices}, got {value!r}\")\n        return value\n\n    return converter\n\n\ndef load_config(file_path: Path) -> dict[str, Any]:\n    \"\"\"Load a nested TOML document into key-value pairs\n\n    E.g. [\"python\"][\"use_venv\"] will be loaded as \"python.use_venv\" key.\n    \"\"\"\n\n    def get_item(sub_data: Mapping[str, Any]) -> dict[str, Any]:\n        result: dict[str, Any] = {}\n        for k, v in sub_data.items():\n            if k in (REPOSITORY, SOURCE):\n                result.update((f\"{k}.{sub_k}\", sub_v) for sub_k, sub_v in v.items())\n            elif isinstance(v, Mapping):\n                result.update({f\"{k}.{sub_k}\": sub_v for sub_k, sub_v in get_item(v).items()})\n            else:\n                result.update({k: v})\n        return result\n\n    try:\n        with file_path.open(\"rb\") as fp:\n            return get_item(tomllib.load(fp))\n    except FileNotFoundError:\n        return {}\n\n\ndef ensure_boolean(val: Any) -> bool:\n    \"\"\"Coerce a string value to a boolean value\"\"\"\n    if not isinstance(val, str):\n        return val\n\n    return bool(val) and val.lower() not in (\"false\", \"no\", \"0\")\n\n\ndef split_by_comma(val: list[str] | str) -> list[str]:\n    \"\"\"Split a string value by comma\"\"\"\n    if isinstance(val, str):\n        return [v.strip() for v in val.split(\",\")]\n    return val\n\n\nDEFAULT_PYPI_INDEX = \"https://pypi.org/simple\"\n\n\n@dataclasses.dataclass\nclass ConfigItem:\n    \"\"\"An item of configuration, with following attributes:\n\n    Args:\n        description (str): the config description\n        default (Any): the default value, if given, will show in `pdm config`\n        global_only (bool): not allowed to save in project config\n        env_var (str|None): the env var name to take value from\n        coerce (Callable): a function to coerce the value\n        replace: (str|None): the deprecated name to replace\n    \"\"\"\n\n    _NOT_SET = object()\n\n    description: str\n    default: Any = _NOT_SET\n    global_only: bool = False\n    env_var: str | None = None\n    coerce: Callable = str\n    replace: str | None = None\n\n    def should_show(self) -> bool:\n        return self.default is not self._NOT_SET\n\n\nclass Config(MutableMapping[str, str]):\n    \"\"\"A dict-like object for configuration key and values\"\"\"\n\n    _config_map: ClassVar[dict[str, ConfigItem]] = {\n        \"cache_dir\": ConfigItem(\n            \"The root directory of cached files\",\n            platformdirs.user_cache_dir(\"pdm\"),\n            True,\n            env_var=\"PDM_CACHE_DIR\",\n        ),\n        \"log_dir\": ConfigItem(\n            \"The root directory of log files\",\n            platformdirs.user_log_dir(\"pdm\"),\n            True,\n            env_var=\"PDM_LOG_DIR\",\n        ),\n        \"check_update\": ConfigItem(\n            \"Check if there is any newer version available\",\n            True,\n            True,\n            env_var=\"PDM_CHECK_UPDATE\",\n            coerce=ensure_boolean,\n        ),\n        \"build_isolation\": ConfigItem(\n            \"Isolate build environment from the project environment\",\n            True,\n            False,\n            \"PDM_BUILD_ISOLATION\",\n            ensure_boolean,\n        ),\n        \"request_timeout\": ConfigItem(\n            \"The timeout for network requests in seconds\", 15, True, \"PDM_REQUEST_TIMEOUT\", coerce=int\n        ),\n        \"use_uv\": ConfigItem(\n            \"Use uv for faster resolution and installation\",\n            False,\n            False,\n            \"PDM_USE_UV\",\n            ensure_boolean,\n        ),\n        \"lock.format\": ConfigItem(\n            \"The format of the lock file, can be `pdm` or `pylock`\",\n            \"pdm\",\n            env_var=\"PDM_LOCK_FORMAT\",\n            coerce=choices(\"pdm\", \"pylock\"),\n        ),\n        \"global_project.fallback\": ConfigItem(\n            \"Use the global project implicitly if no local project is found\",\n            False,\n            True,\n            coerce=ensure_boolean,\n        ),\n        \"global_project.fallback_verbose\": ConfigItem(\n            \"If True show message when global project is used implicitly\",\n            True,\n            True,\n            coerce=ensure_boolean,\n        ),\n        \"global_project.path\": ConfigItem(\n            \"The path to the global project\",\n            platformdirs.user_config_path(\"pdm\") / \"global-project\",\n            True,\n        ),\n        \"global_project.user_site\": ConfigItem(\"Whether to install to user site\", False, True, coerce=ensure_boolean),\n        \"strategy.update\": ConfigItem(\"The default strategy for updating packages\", \"reuse\", False),\n        \"strategy.save\": ConfigItem(\"Specify how to save versions when a package is added\", \"minimum\", False),\n        \"strategy.resolve_max_rounds\": ConfigItem(\n            \"Specify the max rounds of resolution process\",\n            10000,\n            env_var=\"PDM_RESOLVE_MAX_ROUNDS\",\n            coerce=int,\n        ),\n        \"strategy.inherit_metadata\": ConfigItem(\n            \"Inherit the groups and markers from parents for each package\", True, coerce=ensure_boolean\n        ),\n        \"install.parallel\": ConfigItem(\n            \"Whether to perform installation and uninstallation in parallel\",\n            True,\n            env_var=\"PDM_INSTALL_PARALLEL\",\n            coerce=ensure_boolean,\n        ),\n        \"install.cache\": ConfigItem(\n            \"Cache wheel installation and only put symlinks in the library root\",\n            False,\n            coerce=ensure_boolean,\n        ),\n        \"install.cache_method\": ConfigItem(\n            \"Specify how to create links to the caches(`symlink/hardlink`)\",\n            \"symlink\",\n        ),\n        \"python.providers\": ConfigItem(\n            \"List of python provider names for findpython\", default=[], coerce=split_by_comma\n        ),\n        \"python.use_pyenv\": ConfigItem(\"Use the pyenv interpreter\", True, coerce=ensure_boolean),\n        \"python.use_venv\": ConfigItem(\n            \"Use virtual environments when available\", True, env_var=\"PDM_USE_VENV\", coerce=ensure_boolean\n        ),\n        \"python.use_python_version\": ConfigItem(\n            \"Use .python-version file next to pyproject.toml to find python interpreters\",\n            True,\n            env_var=\"PDM_USE_PYTHON_VERSION\",\n            coerce=ensure_boolean,\n        ),\n        \"python.install_root\": ConfigItem(\n            \"The root directory to install python interpreters\",\n            global_only=True,\n            default=os.path.join(platformdirs.user_data_dir(\"pdm\"), \"python\"),\n        ),\n        \"pypi.url\": ConfigItem(\n            \"The URL of PyPI mirror, defaults to https://pypi.org/simple\",\n            DEFAULT_PYPI_INDEX,\n            env_var=\"PDM_PYPI_URL\",\n        ),\n        \"pypi.verify_ssl\": ConfigItem(\n            \"Verify SSL certificate when query PyPI\",\n            True,\n            env_var=\"PDM_PYPI_VERIFY_SSL\",\n            coerce=ensure_boolean,\n        ),\n        \"pypi.username\": ConfigItem(\"The username to access PyPI\", env_var=\"PDM_PYPI_USERNAME\"),\n        \"pypi.password\": ConfigItem(\"The password to access PyPI\", env_var=\"PDM_PYPI_PASSWORD\"),\n        \"pypi.ca_certs\": ConfigItem(\n            \"Path to a CA certificate bundle used for verifying the identity of the PyPI server\", global_only=True\n        ),\n        \"pypi.ignore_stored_index\": ConfigItem(\n            \"Don't add the indexes from the config that is not listed in project\",\n            False,\n            env_var=\"PDM_IGNORE_STORED_INDEX\",\n            coerce=ensure_boolean,\n        ),\n        \"pypi.client_cert\": ConfigItem(\"Path to client certificate file, or combined cert/key file\", global_only=True),\n        \"pypi.client_key\": ConfigItem(\"Path to client cert keyfile, if not in pypi.client_cert\", global_only=True),\n        \"pypi.json_api\": ConfigItem(\n            \"Consult PyPI's JSON API for package metadata\",\n            False,\n            env_var=\"PDM_PYPI_JSON_API\",\n            coerce=ensure_boolean,\n        ),\n        \"scripts.show_header\": ConfigItem(\n            \"Display script name and help before running\",\n            default=False,\n            env_var=\"PDM_SCRIPTS_SHOW_HEADER\",\n            coerce=ensure_boolean,\n        ),\n        \"venv.location\": ConfigItem(\n            \"Parent directory for virtualenvs\",\n            os.path.join(platformdirs.user_data_dir(\"pdm\"), \"venvs\"),\n            global_only=True,\n        ),\n        \"venv.backend\": ConfigItem(\n            \"Default backend to create virtualenv\",\n            default=\"virtualenv\",\n            env_var=\"PDM_VENV_BACKEND\",\n        ),\n        \"venv.in_project\": ConfigItem(\n            \"Create virtualenv in `.venv` under project root\",\n            default=True,\n            env_var=\"PDM_VENV_IN_PROJECT\",\n            coerce=ensure_boolean,\n        ),\n        \"venv.prompt\": ConfigItem(\n            \"Define a custom template to be displayed in the prompt when virtualenv is\"\n            \"active. Variables `project_name` and `python_version` are available for\"\n            \"formatting\",\n            default=\"{project_name}-{python_version}\",\n            env_var=\"PDM_VENV_PROMPT\",\n        ),\n        \"venv.with_pip\": ConfigItem(\n            \"Install pip when creating a new venv\",\n            default=False,\n            env_var=\"PDM_VENV_WITH_PIP\",\n            coerce=ensure_boolean,\n        ),\n    }\n    _config_map.update(\n        (f\"theme.{k}\", ConfigItem(f\"Theme color for {k}\", default=v, global_only=True))\n        for k, v in termui.DEFAULT_THEME.items()\n    )\n\n    site: Config | None = None\n\n    @classmethod\n    def get_defaults(cls) -> dict[str, Any]:\n        defaults = {k: v.default for k, v in cls._config_map.items() if v.should_show()}\n        if cls.site is None:\n            cls.site = Config(platformdirs.site_config_path(\"pdm\") / \"config.toml\")\n        defaults.update(cls.site.self_data)\n        return defaults\n\n    @cached_property\n    def env_map(self) -> Mapping[str, Any]:\n        return EnvMap(self._config_map)\n\n    @classmethod\n    def add_config(cls, name: str, item: ConfigItem) -> None:\n        \"\"\"Add or modify a config item\"\"\"\n        cls._config_map[name] = item\n\n    def __init__(self, config_file: Path, is_global: bool = False):\n        self.is_global = is_global\n        self.config_file = config_file.resolve()\n        self.deprecated = {v.replace: k for k, v in self._config_map.items() if v.replace}\n        self._file_data = load_config(self.config_file)\n        self._data = collections.ChainMap(\n            cast(MutableMapping[str, Any], self.env_map) if not is_global else {},\n            self._file_data,\n            self.get_defaults() if is_global else {},\n        )\n\n    def reload(self) -> None:\n        \"\"\"Reload the config from file\"\"\"\n        new_data = load_config(self.config_file)\n        self._file_data.clear()\n        self._file_data.update(new_data)\n\n    def load_theme(self) -> rich.theme.Theme:\n        if not self.is_global:  # pragma: no cover\n            raise PdmUsageError(\"Theme can only be loaded from global config\")\n        return rich.theme.Theme({k[6:]: v for k, v in self.items() if k.startswith(\"theme.\")})\n\n    @property\n    def self_data(self) -> dict[str, Any]:\n        return dict(self._file_data)\n\n    def iter_sources(self) -> Iterator[RepositoryConfig]:\n        for name, data in self._data.items():\n            if name.startswith(f\"{SOURCE}.\") and name not in self._config_map and data:\n                yield RepositoryConfig(**data, name=name[len(SOURCE) + 1 :], config_prefix=SOURCE)\n\n    def _save_config(self) -> None:\n        import tomlkit\n\n        \"\"\"Save the changed to config file.\"\"\"\n        self.config_file.parent.mkdir(parents=True, exist_ok=True)\n        toml_data: dict[str, Any] = {}\n        for key, value in self._file_data.items():\n            *parts, last = key.split(\".\")\n            temp = toml_data\n            for part in parts:\n                if part not in temp:\n                    temp[part] = {}\n                temp = temp[part]\n            temp[last] = value\n\n        with self.config_file.open(\"w\", encoding=\"utf-8\") as fp:\n            tomlkit.dump(toml_data, fp)\n\n    def __getitem__(self, key: str) -> Any:\n        parts = key.split(\".\")\n        if parts[0] in (REPOSITORY, SOURCE) and key not in self._config_map:\n            if len(parts) < 2:\n                raise PdmUsageError(f\"Must specify a {parts[0]} name\")\n            repo = self.get_repository_config(parts[1], parts[0])\n            if repo is None:\n                raise KeyError(f\"No {parts[0]} named {parts[1]}\")\n            if len(parts) >= 3:\n                repo.populate_keyring_auth()\n                return getattr(repo, parts[2])\n            return repo\n\n        if key not in self._config_map and key not in self.deprecated:\n            raise NoConfigError(key)\n        config_key = self.deprecated.get(key, key)\n        config = self._config_map[config_key]\n\n        if config_key in self._data:\n            result = self._data[config_key]\n        elif config.replace:\n            result = self._data[config.replace]\n        else:\n            raise NoConfigError(key) from None\n        return config.coerce(result)\n\n    def __setitem__(self, key: str, value: Any) -> None:\n        from pdm.models.auth import keyring\n\n        parts = key.split(\".\")\n        if parts[0] in (REPOSITORY, SOURCE) and key not in self._config_map:\n            if len(parts) < 3:\n                raise PdmUsageError(f\"Set {parts[0]} config with [success]{parts[0]}.{{name}}.{{attr}}\")\n            index_key = \".\".join(parts[:2])\n            username = self._data.get(index_key, {}).get(\"username\")  # type: ignore[call-overload]\n            service = f\"pdm-{index_key.replace('.', '-')}\"\n            if (\n                parts[2] == \"password\"\n                and self.is_global\n                and username\n                and keyring.save_auth_info(service, username, value)\n            ):\n                return\n            if parts[2] == \"verify_ssl\":\n                value = ensure_boolean(value)\n            self._file_data.setdefault(index_key, {})[parts[2]] = value\n            self._save_config()\n            return\n\n        if key not in self._config_map and key not in self.deprecated:\n            raise NoConfigError(key)\n        config_key = self.deprecated.get(key, key)\n        config = self._config_map[config_key]\n        if not self.is_global and config.global_only:\n            raise ValueError(f\"Config item '{key}' is not allowed to set in project config.\")\n\n        value = config.coerce(value)\n        if key in self.env_map:\n            ui.warn(f\"the config is shadowed by env var '{config.env_var}', the value set won't take effect.\")\n        self._file_data[config_key] = value\n        if config.replace:\n            self._file_data.pop(config.replace, None)\n        self._save_config()\n\n    def __len__(self) -> int:\n        return len(self._data)\n\n    def __iter__(self) -> Iterator[str]:\n        keys: set[str] = set()\n        for key in self._data:\n            if key in self.deprecated:\n                key = self.deprecated[key]\n            keys.add(key)\n        return iter(keys)\n\n    def __delitem__(self, key: str) -> None:\n        from pdm.models.auth import keyring\n\n        parts = key.split(\".\")\n        if parts[0] in (REPOSITORY, SOURCE) and key not in self._config_map:\n            if len(parts) < 2:\n                raise PdmUsageError(f\"Should specify the name of {parts[0]}\")\n            index_key = \".\".join(parts[:2])\n            username = self._data.get(index_key, {}).get(\"username\")  # type: ignore[call-overload]\n            service = f\"pdm-{index_key.replace('.', '-')}\"\n            if len(parts) >= 3:\n                index_key, attr = key.rsplit(\".\", 1)\n                if attr == \"password\" and username:\n                    keyring.delete_auth_info(service, username)\n                self._file_data.get(index_key, {}).pop(attr, None)\n            else:\n                del self._file_data[key]\n                if username:\n                    keyring.delete_auth_info(service, username)\n            self._save_config()\n            return\n\n        config_key = self.deprecated.get(key, key)\n        config = self._config_map[config_key]\n        self._file_data.pop(config_key, None)\n        if config.replace:\n            self._file_data.pop(config.replace, None)\n\n        env_var = config.env_var\n        if env_var is not None and env_var in os.environ:\n            ui.warn(f\"The config is shadowed by env var '{env_var}', set value won't take effect.\")\n        self._save_config()\n\n    def get_repository_config(self, name_or_url: str, prefix: str) -> RepositoryConfig | None:\n        \"\"\"Get a repository or source by name or url.\"\"\"\n        repositories: dict[str, RepositoryConfig] = {}\n        for k, v in self._data.items():\n            if not k.startswith(f\"{prefix}.\") or k in self._config_map:\n                continue\n            key = k[len(prefix) + 1 :]\n            repositories[key] = RepositoryConfig(**v, name=key, config_prefix=prefix)\n        config: RepositoryConfig | None = None\n        if \"://\" in name_or_url:\n            config = next(\n                (v for v in repositories.values() if v.url == name_or_url),\n                RepositoryConfig(url=name_or_url, name=\"__unknown__\", config_prefix=prefix),\n            )\n        else:\n            config = repositories.get(name_or_url)\n\n        if prefix == SOURCE or not self.is_global:\n            return config\n\n        if name_or_url in DEFAULT_REPOSITORIES:\n            if config is None:\n                return RepositoryConfig(url=DEFAULT_REPOSITORIES[name_or_url], name=name_or_url, config_prefix=prefix)\n            config.passive_update(url=DEFAULT_REPOSITORIES[name_or_url])\n        if name_or_url in DEFAULT_REPOSITORIES.values():\n            name = next(k for k, v in DEFAULT_REPOSITORIES.items() if v == name_or_url)\n            if config is None:\n                return RepositoryConfig(\n                    name=name,\n                    config_prefix=prefix,\n                    url=name_or_url,\n                )\n            config.passive_update(url=name_or_url)\n            if config.name == \"__unknown__\":\n                config.name = name\n        return config\n\n\nclass EnvMap(Mapping[str, Any]):\n    def __init__(self, config_items: Mapping[str, ConfigItem]) -> None:\n        self._config_map = config_items\n\n    def __repr__(self) -> str:\n        return repr(dict(self))\n\n    def __getitem__(self, k: str) -> Any:\n        try:\n            item = self._config_map[k]\n            if item.env_var:\n                return item.coerce(os.environ[item.env_var])\n        except KeyError:\n            pass\n        raise KeyError(k)\n\n    def __iter__(self) -> Iterator[str]:\n        for key, item in self._config_map.items():\n            if item.env_var and item.env_var in os.environ:\n                yield key\n\n    def __len__(self) -> int:\n        return sum(1 for _ in self)\n"
  },
  {
    "path": "src/pdm/project/core.py",
    "content": "from __future__ import annotations\n\nimport contextlib\nimport hashlib\nimport itertools\nimport operator\nimport os\nimport re\nimport shutil\nimport sys\nfrom copy import deepcopy\nfrom functools import cached_property, reduce\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, Callable, Iterable, Mapping, Sequence, cast\n\nimport tomlkit\nfrom pbs_installer import PythonVersion\n\nfrom pdm._types import NotSet, NotSetType, RepositoryConfig\nfrom pdm.compat import CompatibleSequence, tomllib\nfrom pdm.exceptions import NoPythonVersion, PdmUsageError, ProjectError\nfrom pdm.models.backends import DEFAULT_BACKEND, BuildBackend, get_backend_by_spec\nfrom pdm.models.caches import PackageCache\nfrom pdm.models.markers import EnvSpec\nfrom pdm.models.python import PythonInfo\nfrom pdm.models.repositories import BaseRepository, LockedRepository\nfrom pdm.models.requirements import Requirement, parse_line, parse_requirement, strip_extras\nfrom pdm.models.specifiers import PySpecSet\nfrom pdm.project.config import Config, ensure_boolean\nfrom pdm.project.lockfile import FLAG_INHERIT_METADATA, Lockfile, load_lockfile\nfrom pdm.project.project_file import PyProject\nfrom pdm.utils import (\n    cd,\n    deprecation_warning,\n    expand_env_vars_in_auth,\n    find_project_root,\n    find_python_in_path,\n    get_all_installable_python_versions,\n    get_class_init_params,\n    is_conda_base,\n    is_conda_base_python,\n    is_path_relative_to,\n    normalize_name,\n)\n\nif TYPE_CHECKING:\n    from findpython import Finder\n\n    from pdm.core import Core\n    from pdm.environments import BaseEnvironment\n    from pdm.installers.base import BaseSynchronizer\n    from pdm.models.caches import CandidateInfoCache, HashCache, WheelCache\n    from pdm.models.candidates import Candidate\n    from pdm.resolver.base import Resolver\n    from pdm.resolver.providers import BaseProvider\n    from pdm.resolver.reporters import RichLockReporter\n\n\nPYENV_ROOT = os.path.expanduser(os.getenv(\"PYENV_ROOT\", \"~/.pyenv\"))\n\n\nclass Project:\n    \"\"\"Core project class.\n\n    Args:\n        core: The core instance.\n        root_path: The root path of the project.\n        is_global: Whether the project is global.\n        global_config: The path to the global config file.\n    \"\"\"\n\n    PYPROJECT_FILENAME = \"pyproject.toml\"\n    DEPENDENCIES_RE = re.compile(r\"(?:(.+?)-)?dependencies\")\n\n    def __init__(\n        self,\n        core: Core,\n        root_path: str | Path | None,\n        is_global: bool = False,\n        global_config: str | Path | None = None,\n    ) -> None:\n        import platformdirs\n\n        self._lockfile: Lockfile | None = None\n        self._environment: BaseEnvironment | None = None\n        self._python: PythonInfo | None = None\n        self._cache_dir: Path | None = None\n        self.core = core\n\n        if global_config is None:\n            global_config = platformdirs.user_config_path(\"pdm\") / \"config.toml\"\n        self.global_config = Config(Path(global_config), is_global=True)\n        global_project = Path(self.global_config[\"global_project.path\"]).expanduser()\n\n        if root_path is None:\n            root_path = find_project_root() if not is_global else global_project\n        if (\n            not is_global\n            and root_path is None\n            and self.global_config[\"global_project.fallback\"]\n            and not is_conda_base()\n        ):\n            root_path = global_project\n            is_global = True\n            if self.global_config[\"global_project.fallback_verbose\"]:\n                self.core.ui.info(\"Project is not found, fallback to the global project\")\n\n        self.root: Path = Path(root_path or \"\").absolute()\n        self.is_global = is_global\n        self.enable_write_lockfile = os.getenv(\"PDM_FROZEN_LOCKFILE\", os.getenv(\"PDM_NO_LOCK\", \"0\")).lower() not in (\n            \"1\",\n            \"true\",\n        )\n        self.init_global_project()\n\n    def __repr__(self) -> str:\n        return f\"<Project '{self.root.as_posix()}'>\"\n\n    @cached_property\n    def cache_dir(self) -> Path:\n        return Path(self.config.get(\"cache_dir\", \"\")).expanduser()\n\n    @cached_property\n    def pyproject(self) -> PyProject:\n        return PyProject(self.root / self.PYPROJECT_FILENAME, ui=self.core.ui)\n\n    @property\n    def lockfile(self) -> Lockfile:\n        if self._lockfile is None:\n            enable_pylock = self.config[\"lock.format\"] == \"pylock\"\n            if (path := self.root / \"pylock.toml\").exists() and enable_pylock:\n                self.set_lockfile(path)\n            elif (path := self.root / \"pdm.lock\").exists():\n                if enable_pylock:  # pragma: no cover\n                    self.core.ui.warn(\n                        \"`lock.format` is set to pylock but pylock.toml is not found, using pdm.lock instead. \"\n                        \"You can generate pylock with `pdm export -f pylock -o pylock.toml`.\"\n                    )\n                self.set_lockfile(path)\n            else:\n                file_path = \"pylock.toml\" if enable_pylock else \"pdm.lock\"\n                self.set_lockfile(self.root / file_path)\n            assert self._lockfile is not None\n        return self._lockfile\n\n    def set_lockfile(self, path: str | Path) -> None:\n        self._lockfile = load_lockfile(self, path)\n        if self.config.get(\"use_uv\"):\n            self._lockfile.default_strategies.discard(FLAG_INHERIT_METADATA)\n        if not self.config[\"strategy.inherit_metadata\"]:\n            self._lockfile.default_strategies.discard(FLAG_INHERIT_METADATA)\n\n    @cached_property\n    def config(self) -> Mapping[str, Any]:\n        \"\"\"A read-only dict configuration\"\"\"\n        import collections\n\n        return collections.ChainMap(self.project_config, self.global_config)\n\n    @property\n    def scripts(self) -> dict[str, str | dict[str, str]]:\n        return self.pyproject.settings.get(\"scripts\", {})\n\n    @cached_property\n    def project_config(self) -> Config:\n        \"\"\"Read-and-writable configuration dict for project settings\"\"\"\n        config = Config(self.root / \"pdm.toml\")\n        # TODO: for backward compatibility, remove this in the future\n        if self.root.joinpath(\".pdm.toml\").exists():\n            legacy_config = Config(self.root / \".pdm.toml\").self_data\n            config.update((k, v) for k, v in legacy_config.items() if k != \"python.path\")\n        return config\n\n    @property\n    def name(self) -> str:\n        return cast(str, self.pyproject.metadata.get(\"name\"))\n\n    @property\n    def python(self) -> PythonInfo:\n        if not self._python:\n            python = self.resolve_interpreter()\n            if python.major < 3:\n                raise PdmUsageError(\n                    \"Python 2.7 has reached EOL and PDM no longer supports it. \"\n                    \"Please upgrade your Python to 3.6 or later.\",\n                )\n            if self.is_global and is_conda_base_python(python.path):  # pragma: no cover\n                raise PdmUsageError(\"Can't use global project in conda base environment since it is managed by conda\")\n            self._python = python\n        return self._python\n\n    @python.setter\n    def python(self, value: PythonInfo) -> None:\n        self._python = value\n        self._saved_python = value.path.as_posix()\n\n    @property\n    def _saved_python(self) -> str | None:\n        if os.getenv(\"PDM_PYTHON\"):\n            return os.getenv(\"PDM_PYTHON\")\n        with contextlib.suppress(FileNotFoundError):\n            return self.root.joinpath(\".pdm-python\").read_text(\"utf-8\").strip()\n        with contextlib.suppress(FileNotFoundError):\n            # TODO: remove this in the future\n            with self.root.joinpath(\".pdm.toml\").open(\"rb\") as fp:\n                data = tomllib.load(fp)\n                if data.get(\"python\", {}).get(\"path\"):\n                    return data[\"python\"][\"path\"]\n        return None\n\n    @_saved_python.setter\n    def _saved_python(self, value: str | None) -> None:\n        self.root.mkdir(parents=True, exist_ok=True)\n        python_file = self.root.joinpath(\".pdm-python\")\n        if value is None:\n            with contextlib.suppress(FileNotFoundError):\n                python_file.unlink()\n            return\n        python_file.write_text(value, \"utf-8\")\n\n    def resolve_interpreter(self) -> PythonInfo:\n        \"\"\"Get the Python interpreter path.\"\"\"\n        from pdm.cli.commands.venv.utils import iter_venvs\n        from pdm.models.venv import get_venv_python\n\n        def match_version(python: PythonInfo) -> bool:\n            return python.valid and self.python_requires.contains(python.version, True)\n\n        def note(message: str) -> None:\n            if not self.is_global:\n                self.core.ui.info(message)\n\n        def is_active_venv(python: PythonInfo) -> bool:\n            if not (venv := os.getenv(\"VIRTUAL_ENV\", os.getenv(\"CONDA_PREFIX\"))):\n                return False\n            return is_path_relative_to(python.executable, venv)\n\n        config = self.config\n        saved_path = self._saved_python\n        if saved_path and not ensure_boolean(os.getenv(\"PDM_IGNORE_SAVED_PYTHON\")):\n            python = PythonInfo.from_path(saved_path)\n            if match_version(python):\n                return python\n            elif not python.valid:\n                note(\"The saved Python interpreter does not exist or broken. Trying to find another one.\")\n            else:\n                note(\n                    \"The saved Python interpreter doesn't match the project's requirement. Trying to find another one.\"\n                )\n            self._saved_python = None  # Clear the saved path if it doesn't match\n\n        if config.get(\"python.use_venv\") and not self.is_global:\n            # Resolve virtual environments from env-vars\n            ignore_active_venv = ensure_boolean(os.getenv(\"PDM_IGNORE_ACTIVE_VENV\"))\n            venv_in_env = os.getenv(\"VIRTUAL_ENV\", os.getenv(\"CONDA_PREFIX\"))\n            # We don't auto reuse conda's base env since it may cause breakage when removing packages.\n            if not ignore_active_venv and venv_in_env and not is_conda_base():\n                python = PythonInfo.from_path(get_venv_python(Path(venv_in_env)))\n                if match_version(python):\n                    note(\n                        f\"Inside an active virtualenv [success]{venv_in_env}[/], reusing it.\\n\"\n                        \"Set env var [success]PDM_IGNORE_ACTIVE_VENV[/] to ignore it.\"\n                    )\n                    return python\n            # otherwise, get a venv associated with the project\n            for _, venv in iter_venvs(self):\n                python = PythonInfo.from_path(venv.interpreter)\n                if match_version(python) and not (ignore_active_venv and is_active_venv(python)):\n                    note(f\"Virtualenv [success]{venv.root}[/] is reused.\")\n                    self.python = python\n                    return python\n\n            if not self.root.joinpath(\"__pypackages__\").exists():\n                self.core.ui.warn(\n                    f\"Project requires a python version of {self.python_requires}, \"\n                    f\"The virtualenv is being created for you as it cannot be matched to the right version.\"\n                )\n                note(\"python.use_venv is on, creating a virtualenv for this project...\")\n                venv_path = self._create_virtualenv()\n                self.python = PythonInfo.from_path(get_venv_python(venv_path))\n                return self.python\n\n        if self.root.joinpath(\"__pypackages__\").exists() or not config[\"python.use_venv\"] or self.is_global:\n            for py_version in self.iter_interpreters(\n                filter_func=match_version, respect_version_file=config[\"python.use_python_version\"]\n            ):\n                note(\"[success]__pypackages__[/] is detected, using the PEP 582 mode\")\n                self.python = py_version\n                return py_version\n\n        raise NoPythonVersion(f\"No Python that satisfies {self.python_requires} is found on the system.\")\n\n    def get_environment(self) -> BaseEnvironment:\n        from pdm.environments import PythonEnvironment, PythonLocalEnvironment\n\n        \"\"\"Get the environment selected by this project\"\"\"\n\n        if self.is_global:\n            env = PythonEnvironment(self)\n            # Rewrite global project's python requires to be\n            # compatible with the exact version\n            env.python_requires = PySpecSet(f\"=={self.python.version}\")\n            return env\n\n        return (\n            PythonEnvironment(self)\n            if self.config[\"python.use_venv\"] and self.python.get_venv() is not None\n            else PythonLocalEnvironment(self)\n        )\n\n    def _create_virtualenv(self, python: str | None = None) -> Path:\n        from pdm.cli.commands.venv.backends import BACKENDS\n\n        backend: str = self.config[\"venv.backend\"]\n        if backend == \"virtualenv\" and self.config[\"use_uv\"]:\n            backend = \"uv\"\n        venv_backend = BACKENDS[backend](self, python)\n        path = venv_backend.create(\n            force=True,\n            in_project=self.config[\"venv.in_project\"],\n            prompt=self.config[\"venv.prompt\"],\n            with_pip=self.config[\"venv.with_pip\"],\n        )\n        self.core.ui.echo(f\"Virtualenv is created successfully at [success]{path}[/]\", err=True)\n        return path\n\n    @property\n    def environment(self) -> BaseEnvironment:\n        if not self._environment:\n            self._environment = self.get_environment()\n        return self._environment\n\n    @environment.setter\n    def environment(self, value: BaseEnvironment | None) -> None:\n        self._environment = value\n\n    @property\n    def python_requires(self) -> PySpecSet:\n        return PySpecSet(self.pyproject.metadata.get(\"requires-python\", \"\"))\n\n    def get_dependencies(\n        self, group: str | None = None, all_dependencies: dict[str, list[Requirement]] | None = None\n    ) -> Sequence[Requirement]:\n        group = normalize_name(group or \"default\")\n        if all_dependencies is None:\n            all_dependencies = self._resolve_dependencies([group])\n        if group not in all_dependencies:\n            raise ProjectError(f\"Dependency group {group} does not exist\")\n        return CompatibleSequence(all_dependencies[group])\n\n    def iter_groups(self) -> Iterable[str]:\n        groups = {\"default\"}\n        if self.pyproject.metadata.get(\"optional-dependencies\"):\n            groups.update(self.pyproject.metadata[\"optional-dependencies\"].keys())\n        groups.update(self.pyproject._data.get(\"dependency-groups\", {}).keys())\n        groups.update(self.pyproject.settings.get(\"dev-dependencies\", {}).keys())\n        return {normalize_name(g) for g in groups}\n\n    def _resolve_dependencies(\n        self, requested_groups: list[str] | None = None, include_referred: bool = True\n    ) -> dict[str, list[Requirement]]:\n        \"\"\"Resolve dependencies for the given groups, and return a list of requirements for each group.\n\n        The .groups attribute will be set to all that refers this requirement directly or indirectly.\n        If `include_referred` is True, all self-references and `include-group` will be expanded to\n        corresponding requirements. Otherwise, each group only contains explicitly defined requirements.\n        \"\"\"\n\n        def _get_dependencies(group: str) -> tuple[list[Requirement], set[str]]:\n            in_metadata = group in metadata_dependencies\n            collected_deps: list[str] = []\n            referred: set[str] = set()\n            deps = metadata_dependencies.get(group, []) if in_metadata else dev_dependencies[group]\n            for item in deps:\n                if isinstance(item, str):\n                    try:\n                        name, extras = strip_extras(item)\n                    except AssertionError:\n                        pass\n                    else:\n                        if normalize_name(name) == project_name:\n                            if extras:\n                                allowed = (\n                                    set(metadata_dependencies)\n                                    if in_metadata\n                                    else {*metadata_dependencies, *dev_dependencies}\n                                )\n                                extras = tuple(normalize_name(extra) for extra in extras)\n                                not_allowed = set(extras) - allowed\n                                if not_allowed:\n                                    raise ProjectError(\n                                        f\"Optional dependency group '{group}' cannot \"\n                                        f\"include non-existing extras: [{','.join(not_allowed)}]\"\n                                    )\n                                referred.update(extras)\n                            continue\n                    collected_deps.append(item)\n                elif not in_metadata and isinstance(item, dict):\n                    if tuple(item.keys()) != (\"include-group\",):\n                        raise ProjectError(f\"Invalid dependency group item: {item}\")\n                    include_group = normalize_name(item[\"include-group\"])\n                    if include_group not in dev_dependencies:\n                        raise ProjectError(f\"Missing group '{include_group}' in `include-group`\")\n                    referred.add(include_group)\n                else:\n                    raise ProjectError(f\"Invalid dependency in group {group}: {item}\")\n            result: list[Requirement] = []\n            with cd(self.root):\n                for line in collected_deps:\n                    if line.startswith(\"-e \") and in_metadata:\n                        self.core.ui.warn(\n                            f\"Skipping editable dependency [b]{line}[/] in the\"\n                            r\" [success]\\[project][/] table. Please move it to the \"\n                            r\"[success]\\[tool.pdm.dev-dependencies][/] table\"\n                        )\n                        continue\n                    req = parse_line(line)\n                    req.groups = [group]\n                    # make editable packages behind normal ones to override correctly.\n                    result.append(req)\n            return result, referred\n\n        if requested_groups is None:\n            requested_groups = list(self.iter_groups())\n        requested_groups = [normalize_name(g) for g in requested_groups]\n        referred_groups: dict[str, set[str]] = {}\n        metadata_dependencies = {\n            normalize_name(k): v for k, v in self.pyproject.metadata.get(\"optional-dependencies\", {}).items()\n        }\n        if \"default\" in metadata_dependencies:  # pragma: no cover\n            raise ProjectError(\n                \"'default' is reserved by the main dependencies and is not allowed in optional dependencies.\"\n            )\n        metadata_dependencies[\"default\"] = self.pyproject.metadata.get(\"dependencies\", [])\n        dev_dependencies = self.pyproject.dev_dependencies\n        if \"default\" in dev_dependencies:  # pragma: no cover\n            raise ProjectError(\n                \"'default' is reserved by the main dependencies and is not allowed in dependency groups.\"\n            )\n        group_deps: dict[str, list[Requirement]] = {}\n        project_name = normalize_name(self.name) if self.name else None\n        for group in requested_groups:\n            deps, referred = _get_dependencies(group)\n            group_deps[group] = deps\n            if referred:\n                referred_groups[group] = referred\n        extra_deps: dict[str, list[Requirement]] = {}\n        while referred_groups:\n            updated = False\n            ref_iter = list(referred_groups.items())\n            for group, referred in ref_iter:\n                for ref in list(referred):\n                    if ref not in requested_groups:\n                        deps, r = _get_dependencies(ref)\n                        group_deps[ref] = deps\n                        if r:\n                            referred_groups[ref] = r\n                            # append to the ref_iter to process later\n                            ref_iter.append((ref, r))\n                        requested_groups.append(ref)\n                    if ref in referred_groups:  # not resolved yet\n                        continue\n                    extra_deps.setdefault(group, []).extend(group_deps[ref])\n                    for req in itertools.chain(group_deps[ref], extra_deps.get(ref, [])):\n                        if group not in req.groups:\n                            req.groups.append(group)\n                    referred.remove(ref)\n                    updated = True\n                if not referred:\n                    referred_groups.pop(group)\n            if not updated:\n                raise ProjectError(f\"Cyclic dependency group include detected: {set(referred_groups)}\")\n        if include_referred:\n            for group, deps in extra_deps.items():\n                group_deps[group].extend(deps)\n        return group_deps\n\n    @property\n    def all_dependencies(self) -> dict[str, Sequence[Requirement]]:\n        return {k: CompatibleSequence(v) for k, v in self._resolve_dependencies(include_referred=False).items()}\n\n    @property\n    def default_source(self) -> RepositoryConfig:\n        \"\"\"Get the default source from the pypi setting\"\"\"\n        config = RepositoryConfig(\n            config_prefix=\"pypi\",\n            name=\"pypi\",\n            url=self.config[\"pypi.url\"],\n            verify_ssl=self.config[\"pypi.verify_ssl\"],\n            username=self.config.get(\"pypi.username\"),\n            password=self.config.get(\"pypi.password\"),\n            ca_certs=self.config.get(\"pypi.ca_certs\"),\n            client_cert=self.config.get(\"pypi.client_cert\"),\n            client_key=self.config.get(\"pypi.client_key\"),\n        )\n        return config\n\n    @property\n    def sources(self) -> list[RepositoryConfig]:\n        return self.get_sources(include_stored=not self.config.get(\"pypi.ignore_stored_index\", False))\n\n    def get_sources(self, expand_env: bool = True, include_stored: bool = False) -> list[RepositoryConfig]:\n        result: dict[str, RepositoryConfig] = {}\n        for source in self.pyproject.settings.get(\"source\", []):\n            result[source[\"name\"]] = RepositoryConfig(**source, config_prefix=\"pypi\")\n\n        def merge_sources(other_sources: Iterable[RepositoryConfig]) -> None:\n            for source in other_sources:\n                name = source.name\n                if name in result:\n                    result[name].passive_update(source)\n                elif include_stored:\n                    result[name] = source\n\n        merge_sources(self.project_config.iter_sources())\n        merge_sources(self.global_config.iter_sources())\n        if \"pypi\" in result:\n            result[\"pypi\"].passive_update(self.default_source)\n        elif include_stored:\n            # put pypi source at the beginning\n            result = {\"pypi\": self.default_source, **result}\n\n        sources: list[RepositoryConfig] = []\n        for source in result.values():\n            if not source.url:\n                continue\n            if expand_env:\n                source.url = DEFAULT_BACKEND(self.root).expand_line(expand_env_vars_in_auth(source.url))\n            sources.append(source)\n        return sources\n\n    def get_repository(\n        self,\n        cls: type[BaseRepository] | None = None,\n        ignore_compatibility: bool | NotSetType = NotSet,\n        env_spec: EnvSpec | None = None,\n    ) -> BaseRepository:\n        \"\"\"Get the repository object\"\"\"\n        if cls is None:\n            cls = self.core.repository_class\n        sources = self.sources or []\n        params = get_class_init_params(cls)\n        if \"env_spec\" in params:\n            return cls(sources, self.environment, env_spec=env_spec)\n        else:\n            return cls(sources, self.environment, ignore_compatibility=ignore_compatibility)\n\n    def get_locked_repository(self, env_spec: EnvSpec | None = None) -> LockedRepository:\n        try:\n            lockfile = self.lockfile.open_for_read()\n        except ProjectError:\n            lockfile = {}\n\n        return LockedRepository(lockfile, self.sources, self.environment, env_spec=env_spec)\n\n    def split_extras_groups(self, all_groups: list[str]) -> tuple[list[str], list[str]]:\n        \"\"\"Split the groups into extras and non-extras.\"\"\"\n        extras: list[str] = []\n        groups: list[str] = []\n        optional_groups = {normalize_name(group) for group in self.pyproject.metadata.get(\"optional-dependencies\", [])}\n        for group in all_groups:\n            if group in optional_groups:\n                extras.append(group)\n            else:\n                groups.append(group)\n        return extras, groups\n\n    @property\n    def locked_repository(self) -> LockedRepository:\n        deprecation_warning(\"Project.locked_repository is deprecated, use Project.get_locked_repository() instead\", 2)\n        return self.get_locked_repository()\n\n    def get_provider(\n        self,\n        strategy: str = \"all\",\n        tracked_names: Iterable[str] | None = None,\n        for_install: bool = False,\n        ignore_compatibility: bool | NotSetType = NotSet,\n        direct_minimal_versions: bool = False,\n        env_spec: EnvSpec | None = None,\n        locked_repository: LockedRepository | None = None,\n    ) -> BaseProvider:\n        \"\"\"Build a provider class for resolver.\n\n        :param strategy: the resolve strategy\n        :param tracked_names: the names of packages that needs to update\n        :param for_install: if the provider is for install\n        :param ignore_compatibility: if the provider should ignore the compatibility when evaluating candidates\n        :param direct_minimal_versions: if the provider should prefer minimal versions instead of latest\n        :returns: The provider object\n        \"\"\"\n\n        import inspect\n\n        from pdm.resolver.providers import get_provider\n\n        if env_spec is None:\n            env_spec = (\n                self.environment.allow_all_spec if ignore_compatibility in (True, NotSet) else self.environment.spec\n            )\n        repo_params = inspect.signature(self.get_repository).parameters\n        if \"env_spec\" in repo_params:\n            repository = self.get_repository(env_spec=env_spec)\n        else:  # pragma: no cover\n            repository = self.get_repository(ignore_compatibility=ignore_compatibility)\n        if locked_repository is None:\n            try:\n                locked_repository = self.get_locked_repository(env_spec)\n            except Exception:  # pragma: no cover\n                if strategy != \"all\":\n                    self.core.ui.warn(\"Unable to reuse the lock file as it is not compatible with PDM\")\n\n        provider_class = get_provider(strategy)\n        params: dict[str, Any] = {}\n        if strategy != \"all\":\n            params[\"tracked_names\"] = [strip_extras(name)[0] for name in tracked_names or ()]\n        if \"locked_repository\" in inspect.signature(provider_class).parameters:\n            params[\"locked_repository\"] = locked_repository\n        else:\n            locked_candidates: dict[str, list[Candidate]] = (\n                {} if locked_repository is None else locked_repository.all_candidates\n            )\n            params[\"locked_candidates\"] = locked_candidates\n        return provider_class(repository=repository, direct_minimal_versions=direct_minimal_versions, **params)\n\n    def get_reporter(\n        self, requirements: list[Requirement], tracked_names: Iterable[str] | None = None\n    ) -> RichLockReporter:  # pragma: no cover\n        \"\"\"Return the reporter object to construct a resolver.\n\n        :param requirements: requirements to resolve\n        :param tracked_names: the names of packages that needs to update\n        :param spinner: optional spinner object\n        :returns: a reporter\n        \"\"\"\n        from pdm.resolver.reporters import RichLockReporter\n\n        return RichLockReporter(requirements, self.core.ui)\n\n    def write_lockfile(\n        self, toml_data: Any = None, show_message: bool = True, write: bool = True, **_kwds: Any\n    ) -> None:\n        \"\"\"Write the lock file to disk.\"\"\"\n        if _kwds:  # pragma: no cover\n            deprecation_warning(\"Extra arguments have been moved to `format_lockfile` function\", stacklevel=2)\n        if toml_data is not None:  # pragma: no cover\n            deprecation_warning(\n                \"Passing toml_data to write_lockfile is deprecated, please use `format_lockfile` instead\", stacklevel=2\n            )\n            self.lockfile.set_data(toml_data)\n        self.lockfile.update_hash(self.pyproject.content_hash(\"sha256\"))\n        if write and self.enable_write_lockfile:\n            self.lockfile.write(show_message)\n\n    def make_self_candidate(self, editable: bool = True) -> Candidate:\n        from unearth import Link\n\n        from pdm.models.candidates import Candidate\n\n        req = parse_requirement(self.root.as_uri(), editable)\n        assert self.name\n        req.name = self.name\n        can = Candidate(req, name=self.name, link=Link.from_path(self.root))\n        can.prepare(self.environment).metadata\n        return can\n\n    def is_lockfile_hash_match(self) -> bool:\n        algo, hash_value = self.lockfile.hash\n        if not hash_value:\n            return False\n        content_hash = self.pyproject.content_hash(algo)\n        return content_hash == hash_value\n\n    def use_pyproject_dependencies(\n        self, group: str, dev: bool = False\n    ) -> tuple[list[str], Callable[[list[str]], None]]:\n        \"\"\"Get the dependencies array and setter in the pyproject.toml\n        Return a tuple of two elements, the first is the dependencies array,\n        and the second value is a callable to set the dependencies array back.\n        \"\"\"\n        from pdm.formats.base import make_array\n\n        def update_dev_dependencies(deps: list[str]) -> None:\n            from tomlkit.container import OutOfOrderTableProxy\n\n            dependency_groups: list[str | dict[str, str]] = tomlkit.array().multiline(True)\n            dev_dependencies: list[str] = tomlkit.array().multiline(True)\n            for dep in deps:\n                if isinstance(dep, str) and dep.startswith(\"-e\"):\n                    dev_dependencies.append(dep)\n                else:\n                    dependency_groups.append(dep)\n            if dependency_groups:\n                self.pyproject.dependency_groups[group] = dependency_groups\n            else:\n                self.pyproject.dependency_groups.pop(group, None)\n            if dev_dependencies:\n                settings.setdefault(\"dev-dependencies\", {})[group] = dev_dependencies\n            else:\n                settings.setdefault(\"dev-dependencies\", {}).pop(group, None)\n            if isinstance(self.pyproject._data[\"tool\"], OutOfOrderTableProxy):\n                # In case of a separate table, we have to remove and re-add it to make the write correct.\n                # This may change the order of tables in the TOML file, but it's the best we can do.\n                # see bug pdm-project/pdm#2056 for details\n                del self.pyproject._data[\"tool\"][\"pdm\"]\n                self.pyproject._data[\"tool\"][\"pdm\"] = settings\n\n        metadata, settings = self.pyproject.metadata, self.pyproject.settings\n        if group == \"default\":\n            return metadata.get(\"dependencies\", tomlkit.array()), lambda x: metadata.__setitem__(\"dependencies\", x)\n        dev_dependencies = deepcopy(self.pyproject._data.get(\"dependency-groups\", {}))\n        for dev_group, items in self.pyproject.settings.get(\"dev-dependencies\", {}).items():\n            dev_dependencies.setdefault(dev_group, []).extend(items)\n        deps_setter = [\n            (\n                metadata.get(\"optional-dependencies\", {}),\n                lambda x: (\n                    metadata.setdefault(\"optional-dependencies\", {}).__setitem__(group, x)\n                    if x\n                    else metadata.setdefault(\"optional-dependencies\", {}).pop(group, None)\n                ),\n            ),\n            (dev_dependencies, update_dev_dependencies),\n        ]\n        normalized_group = normalize_name(group)\n        for deps, setter in deps_setter:\n            normalized_groups = {normalize_name(g) for g in deps}\n            if group in deps:\n                return make_array(deps[group], True), setter\n            if normalized_group in normalized_groups:\n                raise PdmUsageError(f\"Group {group} already exists in another non-normalized form\")\n        # If not found, return an empty list and a setter to add the group\n        return tomlkit.array().multiline(True), deps_setter[int(dev)][1]\n\n    def add_dependencies(\n        self,\n        requirements: Iterable[str | Requirement],\n        to_group: str = \"default\",\n        dev: bool = False,\n        show_message: bool = True,\n        write: bool = True,\n    ) -> list[Requirement]:\n        \"\"\"Add requirements to the given group, and return the requirements of that group.\"\"\"\n        if isinstance(requirements, Mapping):  # pragma: no cover\n            deprecation_warning(\n                \"Passing a requirements map to add_dependencies is deprecated, please pass an iterable\", stacklevel=2\n            )\n            requirements = requirements.values()\n        self.pyproject.open_for_write()\n        deps, setter = self.use_pyproject_dependencies(to_group, dev)\n        updated_indices: set[int] = set()\n\n        with cd(self.root):\n            parsed_deps = [(parse_line(dep) if isinstance(dep, str) else None) for dep in deps]\n\n            for req in requirements:\n                if isinstance(req, str):\n                    req = parse_line(req)\n                matched_index = next(\n                    (\n                        i\n                        for i, r in enumerate(deps)\n                        if isinstance(r, str) and req.matches(r) and i not in updated_indices\n                    ),\n                    None,\n                )\n                dep = req.as_line()\n                if matched_index is None:\n                    updated_indices.add(len(deps))\n                    deps.append(dep)\n                    parsed_deps.append(req)\n                else:\n                    deps[matched_index] = dep\n                    parsed_deps[matched_index] = req\n                    updated_indices.add(matched_index)\n        setter(deps)\n        if write:\n            self.pyproject.write(show_message)\n        for r in parsed_deps:\n            if r is not None:\n                r.groups = [to_group]\n        return [r for r in parsed_deps if r is not None]\n\n    def init_global_project(self) -> None:\n        if not self.is_global or not self.pyproject.empty():\n            return\n        self.root.mkdir(parents=True, exist_ok=True)\n        self.pyproject.set_data({\"project\": {\"dependencies\": [\"pip\", \"setuptools\", \"wheel\"]}})\n        self.pyproject.write()\n\n    @property\n    def backend(self) -> BuildBackend:\n        return get_backend_by_spec(self.pyproject.build_system)(self.root)\n\n    def cache(self, name: str) -> Path:\n        path = self.cache_dir / name\n        try:\n            path.mkdir(parents=True, exist_ok=True)\n        except OSError:\n            # The path could be not accessible\n            pass\n        return path\n\n    def make_wheel_cache(self) -> WheelCache:\n        from pdm.models.caches import get_wheel_cache\n\n        return get_wheel_cache(self.cache(\"wheels\"))\n\n    @property\n    def package_cache(self) -> PackageCache:\n        return PackageCache(self.cache(\"packages\"))\n\n    def make_candidate_info_cache(self) -> CandidateInfoCache:\n        from pdm.models.caches import CandidateInfoCache, EmptyCandidateInfoCache\n\n        python_hash = hashlib.sha1(str(self.environment.python_requires).encode()).hexdigest()\n        file_name = f\"package_meta_{python_hash}.json\"\n        return (\n            CandidateInfoCache(self.cache(\"metadata\") / file_name)\n            if self.core.state.enable_cache\n            else EmptyCandidateInfoCache(self.cache(\"metadata\") / file_name)\n        )\n\n    def make_hash_cache(self) -> HashCache:\n        from pdm.models.caches import EmptyHashCache, HashCache\n\n        return HashCache(self.cache(\"hashes\")) if self.core.state.enable_cache else EmptyHashCache(self.cache(\"hashes\"))\n\n    def iter_interpreters(\n        self,\n        python_spec: str | None = None,\n        search_venv: bool | None = None,\n        filter_func: Callable[[PythonInfo], bool] | None = None,\n        respect_version_file: bool = True,\n    ) -> Iterable[PythonInfo]:\n        \"\"\"Iterate over all interpreters that matches the given specifier.\n        And optionally install the interpreter if not found.\n        \"\"\"\n        from packaging.version import InvalidVersion\n\n        from pdm.cli.commands.python import InstallCommand\n\n        def read_version_from_version_file(python_version_file: Path) -> str | None:\n            content = python_version_file.read_text().strip()\n            content_lines = [cl for cl in content.splitlines() if not cl.lstrip().startswith(\"#\")]\n\n            return content_lines[0] if len(content_lines) == 1 else None\n\n        version_file = self.root.joinpath(\".python-version\")\n        found = False\n        if respect_version_file and not python_spec and (os.getenv(\"PDM_PYTHON_VERSION\") or version_file.exists()):\n            requested = os.getenv(\"PDM_PYTHON_VERSION\") or read_version_from_version_file(version_file)\n            if requested is not None and requested not in self.python_requires:\n                self.core.ui.warn(\".python-version is found but the version is not in requires-python, ignored.\")\n            elif requested is not None:\n                python_spec = requested\n        for interpreter in self.find_interpreters(python_spec, search_venv):\n            if filter_func is None or filter_func(interpreter):\n                found = True\n                yield interpreter\n        if found or self.is_global:\n            return\n\n        if not python_spec:  # handle both empty string and None\n            # Get the best match meeting the requires-python\n            best_match = self.get_best_matching_cpython_version()\n            if best_match is None:\n                return\n            python_spec = str(best_match)\n        else:\n            try:\n                if python_spec not in self.python_requires:\n                    return\n            except InvalidVersion:\n                return\n        try:\n            # otherwise if no interpreter is found, try to install it\n            installed = InstallCommand.install_python(self, python_spec)\n        except Exception as e:\n            self.core.ui.error(f\"Failed to install Python {python_spec}: {e}\")\n            return\n        else:\n            if filter_func is None or filter_func(installed):\n                yield installed\n\n    def find_interpreters(\n        self, python_spec: str | None = None, search_venv: bool | None = None\n    ) -> Iterable[PythonInfo]:\n        \"\"\"Return an iterable of interpreter paths that matches the given specifier,\n        which can be:\n            1. a version specifier like 3.7\n            2. an absolute path\n            3. a short name like python3\n            4. None that returns all possible interpreters\n        \"\"\"\n        config = self.config\n        python: str | Path | None = None\n        finder_arg: str | None = None\n\n        if not python_spec:\n            if config.get(\"python.use_pyenv\", True) and os.path.exists(PYENV_ROOT):\n                pyenv_shim = os.path.join(PYENV_ROOT, \"shims\", \"python3\")\n                if os.name == \"nt\":\n                    pyenv_shim += \".bat\"\n                if os.path.exists(pyenv_shim):\n                    yield PythonInfo.from_path(pyenv_shim)\n                elif os.path.exists(pyenv_shim.replace(\"python3\", \"python\")):\n                    yield PythonInfo.from_path(pyenv_shim.replace(\"python3\", \"python\"))\n            python = shutil.which(\"python\") or shutil.which(\"python3\")\n            if python:\n                yield PythonInfo.from_path(python)\n        else:\n            if not all(c.isdigit() for c in python_spec.split(\".\")):\n                path = Path(python_spec)\n                if path.exists():\n                    python = find_python_in_path(python_spec)\n                    if python:\n                        yield PythonInfo.from_path(python)\n                        return\n                if len(path.parts) == 1:  # only check for spec with only one part\n                    python = shutil.which(python_spec)\n                    if python:\n                        yield PythonInfo.from_path(python)\n                        return\n            finder_arg = python_spec\n        if search_venv is None:\n            search_venv = cast(bool, config[\"python.use_venv\"])\n        finder = self._get_python_finder(search_venv)\n        for entry in finder.find_all(finder_arg, allow_prereleases=True):\n            yield PythonInfo(entry)\n        if not python_spec:\n            # Lastly, return the host Python as well\n            this_python = getattr(sys, \"_base_executable\", sys.executable)\n            yield PythonInfo.from_path(this_python)\n\n    def _get_python_finder(self, search_venv: bool = True) -> Finder:\n        from findpython import Finder\n\n        from pdm.cli.commands.venv.utils import VenvProvider\n\n        providers: list[str] = self.config[\"python.providers\"]\n        venv_pos = -1\n        if not providers:\n            venv_pos = 0\n        elif \"venv\" in providers:\n            venv_pos = providers.index(\"venv\")\n            providers.remove(\"venv\")\n        old_rye_root = os.getenv(\"RYE_PY_ROOT\")\n        os.environ[\"RYE_PY_ROOT\"] = os.path.expanduser(self.config[\"python.install_root\"])\n        try:\n            finder = Finder(resolve_symlinks=True, selected_providers=providers or None)\n        finally:\n            if old_rye_root:  # pragma: no cover\n                os.environ[\"RYE_PY_ROOT\"] = old_rye_root\n            else:\n                del os.environ[\"RYE_PY_ROOT\"]\n        if search_venv and venv_pos >= 0:\n            finder.add_provider(VenvProvider(self), venv_pos)\n        return finder\n\n    @property\n    def is_distribution(self) -> bool:\n        if not self.name:\n            return False\n        settings = self.pyproject.settings\n        if \"package-type\" in settings:\n            return settings[\"package-type\"] == \"library\"\n        elif \"distribution\" in settings:\n            return cast(bool, settings[\"distribution\"])\n        else:\n            return True\n\n    def get_setting(self, key: str) -> Any:\n        \"\"\"\n        Get a setting from its dotted key (without the `tool.pdm` prefix).\n\n        Returns `None` if the key does not exists.\n        \"\"\"\n        try:\n            return reduce(operator.getitem, key.split(\".\"), self.pyproject.settings)\n        except KeyError:\n            return None\n\n    def env_or_setting(self, var: str, key: str) -> Any:\n        \"\"\"\n        Get a value from environment variable and fallback on a given setting.\n\n        Returns `None` if both the environment variable and the key does not exists.\n        \"\"\"\n        return os.getenv(var.upper()) or self.get_setting(key)\n\n    def get_best_matching_cpython_version(\n        self, use_minimum: bool | None = False, freethreaded: bool = False\n    ) -> PythonVersion | None:\n        \"\"\"\n        Returns the best matching CPython version that fits requires-python, this platform and arch.\n        If no best match could be found, return None.\n\n        Default for best match strategy is \"highest\" possible interpreter version. If \"minimum\" shall be used,\n        set `use_minimum` to True.\n        \"\"\"\n\n        def get_version(version: PythonVersion) -> str:\n            return f\"{version.major}.{version.minor}.{version.micro}\"\n\n        all_matches = get_all_installable_python_versions(build_dir=False)\n        filtered_matches = [\n            v\n            for v in all_matches\n            if v.freethreaded == freethreaded\n            and get_version(v) in self.python_requires\n            and v.implementation.lower() == \"cpython\"\n        ]\n        if filtered_matches:\n            if use_minimum:\n                return min(filtered_matches, key=lambda v: (v.major, v.minor, v.micro))\n            return max(filtered_matches, key=lambda v: (v.major, v.minor, v.micro))\n\n        return None\n\n    @property\n    def lock_targets(self) -> list[EnvSpec]:\n        return [self.environment.allow_all_spec]\n\n    def get_resolver(self, allow_uv: bool = True) -> type[Resolver]:\n        \"\"\"Get the resolver class to use for the project.\"\"\"\n        from pdm.resolver.resolvelib import RLResolver\n        from pdm.resolver.uv import UvResolver\n\n        if allow_uv and self.config.get(\"use_uv\"):\n            return UvResolver\n        else:\n            return RLResolver\n\n    def get_synchronizer(self, quiet: bool = False, allow_uv: bool = True) -> type[BaseSynchronizer]:\n        \"\"\"Get the synchronizer class to use for the project.\"\"\"\n        from pdm.installers import BaseSynchronizer, Synchronizer, UvSynchronizer\n        from pdm.installers.uv import QuietUvSynchronizer\n\n        if allow_uv and self.config.get(\"use_uv\"):\n            return QuietUvSynchronizer if quiet else UvSynchronizer\n        if quiet:\n            return BaseSynchronizer\n        return getattr(self.core, \"synchronizer_class\", Synchronizer)\n"
  },
  {
    "path": "src/pdm/project/lockfile/__init__.py",
    "content": "from __future__ import annotations\n\nimport sys\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nfrom pdm.project.lockfile.base import (\n    FLAG_CROSS_PLATFORM,\n    FLAG_DIRECT_MINIMAL_VERSIONS,\n    FLAG_INHERIT_METADATA,\n    FLAG_STATIC_URLS,\n    Lockfile,\n)\nfrom pdm.project.lockfile.pdmlock import PDMLock\nfrom pdm.project.lockfile.pylock import PyLock\n\nif sys.version_info >= (3, 11):\n    import tomllib\nelse:\n    import tomli as tomllib\n\n\nif TYPE_CHECKING:\n    from pdm.project import Project\n\n__all__ = [\n    \"FLAG_CROSS_PLATFORM\",\n    \"FLAG_DIRECT_MINIMAL_VERSIONS\",\n    \"FLAG_INHERIT_METADATA\",\n    \"FLAG_STATIC_URLS\",\n    \"Lockfile\",\n    \"PDMLock\",\n    \"PyLock\",\n    \"load_lockfile\",\n]\n\n\ndef load_lockfile(project: Project, path: str | Path) -> Lockfile:\n    \"\"\"Load a lockfile from the given path.\"\"\"\n\n    default_lockfile = PyLock if project.config[\"lock.format\"] == \"pylock\" else PDMLock\n\n    try:\n        with open(path, \"rb\") as f:\n            data = tomllib.load(f)\n    except OSError:\n        return default_lockfile(path, ui=project.core.ui)\n    else:\n        klass: type[Lockfile]\n        if data.get(\"metadata\", {}).get(\"lock_version\"):\n            klass = PDMLock\n        elif data.get(\"lock-version\"):\n            klass = PyLock\n        else:  # pragma: no cover\n            klass = default_lockfile\n        lockfile = klass(path, ui=project.core.ui, parse=False)\n        lockfile._data = data  # type: ignore[assignment]\n        return lockfile\n"
  },
  {
    "path": "src/pdm/project/lockfile/base.py",
    "content": "from __future__ import annotations\n\nimport abc\nimport enum\nfrom functools import cached_property\nfrom typing import TYPE_CHECKING, AbstractSet, Any, Iterable, Mapping\n\nimport tomlkit\n\nfrom pdm import termui\nfrom pdm.exceptions import PdmUsageError\nfrom pdm.project.toml_file import TOMLFile\n\nif TYPE_CHECKING:\n    from pdm.models.repositories import LockedRepository\n\nGENERATED_COMMENTS = [\n    \"This file is @generated by PDM.\",\n    \"It is not intended for manual editing.\",\n]\nFLAG_STATIC_URLS = \"static_urls\"\nFLAG_CROSS_PLATFORM = \"cross_platform\"\nFLAG_DIRECT_MINIMAL_VERSIONS = \"direct_minimal_versions\"\nFLAG_INHERIT_METADATA = \"inherit_metadata\"\n\n\nclass Compatibility(enum.IntEnum):\n    NONE = 0  # The lockfile can't be read by the current version of PDM.\n    SAME = 1  # The lockfile version is the same as the current version of PDM.\n    BACKWARD = 2  # The current version of PDM is newer than the lockfile version.\n    FORWARD = 3  # The current version of PDM is older than the lockfile version.\n\n\nclass Lockfile(TOMLFile, metaclass=abc.ABCMeta):\n    SUPPORTED_FLAGS: AbstractSet[str]\n\n    @property\n    @abc.abstractmethod\n    def hash(self) -> tuple[str, str]:\n        \"\"\"The content hash algo and hash value of the pyproject.toml to generate this lockfile.\"\"\"\n        pass\n\n    @abc.abstractmethod\n    def update_hash(self, hash_value: str, algo: str = \"sha256\") -> None:\n        \"\"\"Update the content hash of the lockfile.\"\"\"\n        pass\n\n    @property\n    @abc.abstractmethod\n    def groups(self) -> list[str] | None:\n        \"\"\"The groups defined in the lockfile, or None if no groups are defined.\"\"\"\n        return []\n\n    @property\n    @abc.abstractmethod\n    def strategy(self) -> set[str]:\n        \"\"\"The strategies used in the lockfile.\"\"\"\n        return set()\n\n    @cached_property\n    @abc.abstractmethod\n    def default_strategies(self) -> set[str]:\n        \"\"\"The default strategies to be used if no strategies are defined in the lockfile.\"\"\"\n        return set()\n\n    def apply_strategy_change(self, changes: Iterable[str]) -> set[str]:\n        \"\"\"Apply the given strategy changes to the current strategy.\"\"\"\n        original = self.strategy\n        supported = self.SUPPORTED_FLAGS\n        for change in changes:\n            change = change.replace(\"-\", \"_\").lower()\n            if change.startswith(\"no_\"):\n                if change[3:] not in supported:\n                    raise PdmUsageError(f\"Invalid strategy flag: {change[3:]}, supported: {', '.join(supported)}\")\n                original.discard(change[3:])\n            else:\n                if change not in supported:\n                    raise PdmUsageError(f\"Invalid strategy flag: {change}, supported: {', '.join(supported)}\")\n                original.add(change)\n        return original\n\n    def compare_groups(self, groups: Iterable[str]) -> list[str]:\n        \"\"\"Compare the given groups with the lockfile groups and return the groups that are not in the lockfile.\"\"\"\n        if not self.groups:\n            return []\n        return list(set(groups).difference(self.groups))\n\n    def set_data(self, data: Mapping[str, Any]) -> None:\n        doc = tomlkit.document()\n        for line in GENERATED_COMMENTS:\n            doc.append(None, tomlkit.comment(line))\n        doc.update(data)\n        super().set_data(doc)\n\n    def write(self, show_message: bool = True) -> None:\n        super().write()\n        if show_message:\n            self.ui.echo(f\"Changes are written to [success]{self._path.name}[/].\", verbosity=termui.Verbosity.NORMAL)\n\n    def __getitem__(self, key: str) -> dict:\n        return self._data[key]  # type: ignore[return-value]\n\n    @abc.abstractmethod\n    def compatibility(self) -> Compatibility:\n        \"\"\"We use a three-part versioning scheme for lockfiles:\n        The first digit represents backward compatibility and the second digit represents forward compatibility.\n        \"\"\"\n\n    @abc.abstractmethod\n    def format_lockfile(self, repository: LockedRepository, groups: Iterable[str] | None, strategy: set[str]) -> None:\n        \"\"\"Format lock file from a dict of resolved candidates, a mapping of dependencies\n        and a collection of package summaries.\n        \"\"\"\n"
  },
  {
    "path": "src/pdm/project/lockfile/pdmlock.py",
    "content": "from __future__ import annotations\n\nfrom functools import cached_property\nfrom typing import TYPE_CHECKING, Iterable\n\nimport tomlkit\n\nfrom pdm.project.lockfile.base import (\n    FLAG_CROSS_PLATFORM,\n    FLAG_DIRECT_MINIMAL_VERSIONS,\n    FLAG_INHERIT_METADATA,\n    FLAG_STATIC_URLS,\n    Compatibility,\n    Lockfile,\n)\nfrom pdm.utils import parse_version\n\nif TYPE_CHECKING:\n    from pdm.models.repositories import LockedRepository\n\n\nclass PDMLock(Lockfile):\n    SUPPORTED_FLAGS = frozenset(\n        (FLAG_STATIC_URLS, FLAG_CROSS_PLATFORM, FLAG_DIRECT_MINIMAL_VERSIONS, FLAG_INHERIT_METADATA)\n    )\n    spec_version = parse_version(\"4.5.0\")\n\n    @property\n    def hash(self) -> tuple[str, str]:\n        content_hash = self._data.get(\"metadata\", {}).get(\"content_hash\", \"\")\n        return content_hash.split(\":\", 1)\n\n    @property\n    def file_version(self) -> str:\n        return self._data.get(\"metadata\", {}).get(\"lock_version\", \"\")\n\n    @property\n    def groups(self) -> list[str] | None:\n        return self._data.get(\"metadata\", {}).get(\"groups\")\n\n    @cached_property\n    def default_strategies(self) -> set[str]:\n        return {FLAG_INHERIT_METADATA}\n\n    @property\n    def strategy(self) -> set[str]:\n        metadata = self._data.get(\"metadata\", {})\n        if not metadata:\n            return self.default_strategies.copy()\n        result: set[str] = set(metadata.get(\"strategy\", {FLAG_CROSS_PLATFORM}))\n        # Compatibility with old lockfiles\n        if not metadata.get(FLAG_CROSS_PLATFORM, True):\n            result.discard(FLAG_CROSS_PLATFORM)\n        if metadata.get(FLAG_STATIC_URLS, False):\n            result.add(FLAG_STATIC_URLS)\n        return result & self.SUPPORTED_FLAGS\n\n    def update_hash(self, hash_value: str, algo: str = \"sha256\") -> None:\n        self._data.setdefault(\"metadata\", {})[\"content_hash\"] = f\"{algo}:{hash_value}\"\n\n    def compatibility(self) -> Compatibility:\n        \"\"\"We use a three-part versioning scheme for lockfiles:\n        The first digit represents backward compatibility and the second digit represents forward compatibility.\n        \"\"\"\n        if not self.exists():\n            return Compatibility.SAME\n        if not self.file_version:\n            return Compatibility.NONE\n        lockfile_version = parse_version(self.file_version)\n        if lockfile_version == self.spec_version:\n            return Compatibility.SAME\n        if lockfile_version.major != self.spec_version.major or lockfile_version.minor > self.spec_version.minor:\n            return Compatibility.NONE\n        if lockfile_version.minor < self.spec_version.minor:\n            return Compatibility.BACKWARD\n        return Compatibility.BACKWARD if lockfile_version.micro < self.spec_version.micro else Compatibility.FORWARD\n\n    def format_lockfile(self, repository: LockedRepository, groups: Iterable[str] | None, strategy: set[str]) -> None:\n        \"\"\"Format lock file from a dict of resolved candidates, a mapping of dependencies\n        and a collection of package summaries.\n        \"\"\"\n        from pdm.formats.base import make_array, make_inline_table\n\n        def _group_sort_key(group: str) -> tuple[bool, str]:\n            return group != \"default\", group\n\n        project = repository.environment.project\n        packages = tomlkit.aot()\n        for entry in sorted(repository.packages.values(), key=lambda x: x.candidate.identify()):\n            base = tomlkit.table()\n            base.update(entry.candidate.as_lockfile_entry(project.root))\n            base.add(\"summary\", entry.summary or \"\")\n            if FLAG_INHERIT_METADATA in strategy:\n                base.add(\"groups\", sorted(entry.candidate.req.groups, key=_group_sort_key))\n                if entry.candidate.req.marker is not None:\n                    base.add(\"marker\", str(entry.candidate.req.marker))\n            if entry.dependencies:\n                base.add(\"dependencies\", make_array(sorted(entry.dependencies), True))\n            if hashes := entry.candidate.hashes:\n                collected = {}\n                for item in hashes:\n                    if FLAG_STATIC_URLS in strategy:\n                        row = {\"url\": item[\"url\"], \"hash\": item[\"hash\"]}\n                    else:\n                        row = {\"file\": item[\"file\"], \"hash\": item[\"hash\"]}\n                    inline = make_inline_table(row)\n                    # deduplicate and sort\n                    collected[tuple(row.values())] = inline\n                if collected:\n                    base.add(\"files\", make_array([collected[k] for k in sorted(collected)], True))\n            packages.append(base)\n        doc = tomlkit.document()\n        metadata = tomlkit.table()\n        if groups is None:\n            groups = list(project.iter_groups())\n        metadata.update(\n            {\n                \"groups\": sorted(groups, key=_group_sort_key),\n                \"strategy\": sorted(strategy),\n                \"targets\": [t.as_dict() for t in repository.targets],\n                \"lock_version\": str(self.spec_version),\n            }\n        )\n        metadata.pop(FLAG_STATIC_URLS, None)\n        metadata.pop(FLAG_CROSS_PLATFORM, None)\n        doc.add(\"metadata\", metadata)\n        doc.add(\"package\", packages)\n        self.set_data(doc)\n"
  },
  {
    "path": "src/pdm/project/lockfile/pylock.py",
    "content": "from __future__ import annotations\n\nfrom functools import cached_property\nfrom typing import Iterable\n\nfrom pdm.exceptions import PdmUsageError\nfrom pdm.models.repositories.lock import LockedRepository\nfrom pdm.project.lockfile.base import (\n    FLAG_DIRECT_MINIMAL_VERSIONS,\n    FLAG_INHERIT_METADATA,\n    FLAG_STATIC_URLS,\n    Compatibility,\n    Lockfile,\n)\n\n\nclass PyLock(Lockfile):\n    SUPPORTED_FLAGS = frozenset([FLAG_DIRECT_MINIMAL_VERSIONS, FLAG_INHERIT_METADATA, FLAG_STATIC_URLS])\n\n    @property\n    def hash(self) -> tuple[str, str]:\n        return next(iter(self._data.get(\"tool\", {}).get(\"pdm\", {}).get(\"hashes\", {}).items()), (\"\", \"\"))\n\n    def update_hash(self, hash_value: str, algo: str = \"sha256\") -> None:\n        self._data.setdefault(\"tool\", {}).setdefault(\"pdm\", {}).setdefault(\"hashes\", {})[algo] = hash_value\n\n    @property\n    def groups(self) -> list[str] | None:\n        return [*self._data.get(\"dependency-groups\", []), *self._data.get(\"extras\", [])]\n\n    @cached_property\n    def default_strategies(self) -> set[str]:\n        return {FLAG_INHERIT_METADATA, FLAG_STATIC_URLS}\n\n    @property\n    def strategy(self) -> set[str]:\n        return set(self._data.get(\"tool\", {}).get(\"pdm\", {}).get(\"strategy\", self.default_strategies))\n\n    def apply_strategy_change(self, changes: Iterable[str]) -> set[str]:\n        for change in changes:\n            change = change.replace(\"-\", \"_\").lower()\n            if change.startswith(\"no_\") and change[3:] != FLAG_DIRECT_MINIMAL_VERSIONS:\n                raise PdmUsageError(f\"Unsupported strategy change for pylock: {change}\")\n        return super().apply_strategy_change(changes)\n\n    def format_lockfile(self, repository: LockedRepository, groups: Iterable[str] | None, strategy: set[str]) -> None:\n        from pdm.formats.pylock import PyLockConverter\n\n        converter = PyLockConverter(repository.environment.project, repository)\n        data = converter.convert(groups)\n        data[\"tool\"][\"pdm\"][\"strategy\"] = sorted(strategy)\n        self.set_data(data)\n\n    def compatibility(self) -> Compatibility:  # pragma: no cover\n        return Compatibility.SAME\n"
  },
  {
    "path": "src/pdm/project/project_file.py",
    "content": "from __future__ import annotations\n\nimport hashlib\nimport json\nfrom typing import Any, cast\n\nimport tomlkit\n\nfrom pdm import termui\nfrom pdm.exceptions import ProjectError\nfrom pdm.project.toml_file import TOMLFile\nfrom pdm.utils import normalize_name\n\n\ndef _remove_empty_tables(doc: dict) -> None:\n    for k, v in list(doc.items()):\n        if isinstance(v, dict):\n            _remove_empty_tables(v)\n            if not v:\n                del doc[k]\n\n\nclass PyProject(TOMLFile):\n    \"\"\"The data object representing th pyproject.toml file\"\"\"\n\n    def _parse(self) -> dict[str, Any]:\n        data = super()._parse()\n        self._convert_pyproject(data)\n        return data\n\n    def open_for_write(self) -> tomlkit.TOMLDocument:\n        if self._for_write:\n            return cast(tomlkit.TOMLDocument, self._data)\n        doc = super().open_for_write()\n        self._convert_pyproject(doc)\n        return doc\n\n    def _convert_pyproject(self, data: dict[str, Any]) -> None:\n        from pdm.formats import flit, poetry\n\n        if \"project\" not in data and self._path.exists():\n            # Try converting from flit and poetry\n            for converter in (flit, poetry):\n                if converter.check_fingerprint(None, self._path):\n                    metadata, settings = converter.convert(None, self._path, None)\n                    data[\"project\"] = metadata\n                    if settings:\n                        data.setdefault(\"tool\", {}).setdefault(\"pdm\", {}).update(settings)\n                    break\n\n    def write(self, show_message: bool = True) -> None:\n        \"\"\"Write the TOMLDocument to the file.\"\"\"\n        _remove_empty_tables(self._data.get(\"project\", {}))\n        if \"tool\" in self._data:\n            tool_table = cast(dict, self._data[\"tool\"])\n            _remove_empty_tables(tool_table.get(\"pdm\", {}))\n            if \"pdm\" in tool_table and not tool_table[\"pdm\"]:\n                del tool_table[\"pdm\"]\n            if not tool_table:\n                del self._data[\"tool\"]\n\n        if \"dependency-groups\" in self._data and not self.dependency_groups:\n            del self._data[\"dependency-groups\"]\n        super().write()\n        if show_message:\n            self.ui.echo(\"Changes are written to [success]pyproject.toml[/].\", verbosity=termui.Verbosity.NORMAL)\n\n    @property\n    def is_valid(self) -> bool:\n        return bool(self._data.get(\"project\"))\n\n    @property\n    def metadata(self) -> dict[str, Any]:\n        return self._data.setdefault(\"project\", {})\n\n    @property\n    def dependency_groups(self) -> dict[str, Any]:\n        return self._data.setdefault(\"dependency-groups\", {})\n\n    @property\n    def dev_dependencies(self) -> dict[str, list[Any]]:\n        groups: dict[str, list[Any]] = {}\n        for group, deps in self._data.get(\"dependency-groups\", {}).items():\n            group = normalize_name(group)\n            if group in groups:\n                raise ProjectError(f\"The group {group} is duplicated in dependency-groups\")\n            groups[group] = deps.unwrap() if hasattr(deps, \"unwrap\") else deps\n        for group, deps in self.settings.get(\"dev-dependencies\", {}).items():\n            group = normalize_name(group)\n            groups.setdefault(group, []).extend(deps.unwrap() if hasattr(deps, \"unwrap\") else deps)\n        return groups\n\n    @property\n    def settings(self) -> dict[str, Any]:\n        return self._data.setdefault(\"tool\", {}).setdefault(\"pdm\", {})\n\n    @property\n    def build_system(self) -> dict[str, Any]:\n        return self._data.get(\"build-system\", {})\n\n    @property\n    def resolution(self) -> dict[str, Any]:\n        \"\"\"A compatible getter method for the resolution overrides\n        in the pyproject.toml file.\n        \"\"\"\n        return self.settings.get(\"resolution\", {})\n\n    @property\n    def allow_prereleases(self) -> bool | None:\n        return self.resolution.get(\"allow-prereleases\")\n\n    def content_hash(self, algo: str = \"sha256\") -> str:\n        \"\"\"Generate a hash of the sensible content of the pyproject.toml file.\n        When the hash changes, it means the project needs to be relocked.\n        \"\"\"\n        dump_data = {\n            \"sources\": self.settings.get(\"source\", []),\n            \"dependencies\": self.metadata.get(\"dependencies\", []),\n            \"dev-dependencies\": self.dev_dependencies,\n            \"optional-dependencies\": self.metadata.get(\"optional-dependencies\", {}),\n            \"requires-python\": self.metadata.get(\"requires-python\", \"\"),\n            \"resolution\": self.resolution,\n        }\n        pyproject_content = json.dumps(dump_data, sort_keys=True)\n        hasher = hashlib.new(algo)\n        hasher.update(pyproject_content.encode(\"utf-8\"))\n        return hasher.hexdigest()\n\n    @property\n    def plugins(self) -> list[str]:\n        return self.settings.get(\"plugins\", [])\n"
  },
  {
    "path": "src/pdm/project/toml_file.py",
    "content": "from __future__ import annotations\n\nfrom copy import deepcopy\nfrom pathlib import Path\nfrom typing import Any, cast\n\nimport tomlkit\n\nfrom pdm import termui\nfrom pdm.compat import tomllib\n\n\nclass TOMLFile:\n    def __init__(self, path: str | Path, *, parse: bool = True, ui: termui.UI) -> None:\n        from tomlkit.toml_file import TOMLFile as TomlkitTOMLFile\n\n        self._file = TomlkitTOMLFile(path)\n        self.ui = ui\n        self._data = self._parse() if parse else {}\n        self._for_write = False\n\n    @property\n    def _path(self) -> Path:\n        return Path(self._file._path)\n\n    def _parse(self) -> dict[str, Any]:\n        # By default, use tomllib for parsing as it is much faster\n        try:\n            with open(self._path, \"rb\") as fp:\n                return tomllib.load(fp)\n        except FileNotFoundError:\n            return {}\n\n    def open_for_write(self) -> tomlkit.TOMLDocument:\n        # Ensure the document is re-parsed by tomlkit for writing with styles preserved\n        if self._for_write:\n            return cast(tomlkit.TOMLDocument, self._data)\n        try:\n            self._data = self._file.read()\n        except FileNotFoundError:\n            self._data = tomlkit.document()\n        self._for_write = True\n        return self._data\n\n    def open_for_read(self) -> dict[str, Any]:\n        \"\"\"Get the (read-only) data of the TOML file.\"\"\"\n        if hasattr(self._data, \"unwrap\"):\n            return self._data.unwrap()  # type: ignore[attr-defined]\n        return deepcopy(self._data)\n\n    def set_data(self, data: dict[str, Any]) -> None:\n        \"\"\"Set the data of the TOML file.\"\"\"\n        self._data = data\n        self._for_write = True\n\n    def reload(self) -> None:\n        self._data = self._parse()\n        self._for_write = False\n\n    def write(self) -> None:\n        if not self._for_write:\n            raise RuntimeError(\"TOMLFile not opened for write. Call open_for_write() first.\")\n        self._path.parent.mkdir(parents=True, exist_ok=True)\n        if isinstance(self._data, tomlkit.TOMLDocument):\n            data = self._data\n        else:\n            data = tomlkit.document()\n            data.update(self._data)\n        self._file.write(data)\n\n    def exists(self) -> bool:\n        return self._path.exists()\n\n    def empty(self) -> bool:\n        return not self._data\n"
  },
  {
    "path": "src/pdm/py.typed",
    "content": ""
  },
  {
    "path": "src/pdm/pytest.py",
    "content": "\"\"\"\nSome reusable fixtures for `pytest`.\n\n+++ 2.4.0\n\nTo enable them in your test, add `pdm.pytest` as a plugin.\nYou can do so in your root `conftest.py`:\n\n```python title=\"conftest.py\"\n# single plugin\npytest_plugins = \"pytest.plugin\"\n\n# many plugins\npytest_plugins = [\n    ...\n    \"pdm.pytest\",\n    ...\n]\n```\n\"\"\"\n\nfrom __future__ import annotations\n\nimport collections.abc\nimport json\nimport os\nimport shutil\nimport sys\nfrom dataclasses import dataclass\nfrom io import StringIO\nfrom pathlib import Path\nfrom typing import (\n    TYPE_CHECKING,\n    Any,\n    Callable,\n    Generator,\n    Iterable,\n    Iterator,\n    Mapping,\n    MutableMapping,\n    Union,\n    cast,\n)\n\nimport httpx\nimport pytest\nfrom httpx._content import IteratorByteStream\nfrom pytest_mock import MockerFixture\nfrom unearth import Link\n\nfrom pdm.core import Core\nfrom pdm.environments import BaseEnvironment, PythonEnvironment\nfrom pdm.exceptions import CandidateInfoNotFound\nfrom pdm.installers.installers import install_wheel\nfrom pdm.models.backends import DEFAULT_BACKEND\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.repositories import BaseRepository, CandidateMetadata\nfrom pdm.models.requirements import (\n    Requirement,\n    filter_requirements_with_extras,\n    parse_requirement,\n)\nfrom pdm.models.session import PDMPyPIClient\nfrom pdm.project.config import Config\nfrom pdm.project.core import Project\nfrom pdm.utils import normalize_name, parse_version\n\nif TYPE_CHECKING:\n    from typing import Protocol\n\n    from _pytest.fixtures import SubRequest\n\n    from pdm._types import FileHash\n\n\nclass FileByteStream(IteratorByteStream):\n    def close(self) -> None:\n        self._stream.close()  # type: ignore[attr-defined]\n\n\nclass LocalIndexTransport(httpx.BaseTransport):\n    \"\"\"\n    A local file transport for HTTPX.\n\n    Allows to mock some HTTP requests with some local files\n    \"\"\"\n\n    def __init__(\n        self,\n        aliases: dict[str, Path],\n        overrides: IndexOverrides | None = None,\n        strip_suffix: bool = False,\n    ):\n        super().__init__()\n        self.aliases = sorted(aliases.items(), key=lambda item: len(item[0]), reverse=True)\n        self.overrides = overrides if overrides is not None else {}\n        self.strip_suffix = strip_suffix\n\n    def get_file_path(self, path: str) -> Path | None:\n        for prefix, base_path in self.aliases:\n            if path.startswith(prefix):\n                file_path = base_path / path[len(prefix) :].lstrip(\"/\")\n                if not self.strip_suffix:\n                    return file_path\n                return next(\n                    (p for p in file_path.parent.iterdir() if p.stem == file_path.name),\n                    None,\n                )\n        return None\n\n    def handle_request(self, request: httpx.Request) -> httpx.Response:\n        request_path = request.url.path\n        file_path = self.get_file_path(request_path)\n        headers: dict[str, str] = {}\n        stream: httpx.SyncByteStream | None = None\n        content: bytes | None = None\n        if request_path in self.overrides:\n            status_code = 200\n            content = self.overrides[request_path]\n            headers[\"Content-Type\"] = \"text/html\"\n        elif file_path is None or not file_path.exists():\n            status_code = 404\n        else:\n            status_code = 200\n            stream = FileByteStream(file_path.open(\"rb\"))\n            if file_path.suffix == \".html\":\n                headers[\"Content-Type\"] = \"text/html\"\n            elif file_path.suffix == \".json\":\n                headers[\"Content-Type\"] = \"application/vnd.pypi.simple.v1+json\"\n        return httpx.Response(status_code, headers=headers, content=content, stream=stream)\n\n\nclass RepositoryData:\n    def __init__(self, pypi_json: Path) -> None:\n        self.pypi_data = self.load_fixtures(pypi_json)\n\n    @staticmethod\n    def load_fixtures(pypi_json: Path) -> dict[str, Any]:\n        return json.loads(pypi_json.read_text())\n\n    def add_candidate(self, name: str, version: str, requires_python: str = \"\") -> None:\n        pypi_data = self.pypi_data.setdefault(normalize_name(name), {}).setdefault(version, {})\n        pypi_data[\"requires_python\"] = requires_python\n\n    def add_dependencies(self, name: str, version: str, requirements: list[str]) -> None:\n        pypi_data = self.pypi_data[normalize_name(name)][version]\n        pypi_data.setdefault(\"dependencies\", []).extend(requirements)\n\n    def get_raw_dependencies(self, candidate: Candidate) -> tuple[str, list[str]]:\n        try:\n            pypi_data = self.pypi_data[cast(str, candidate.req.key)]\n            for version, data in sorted(pypi_data.items(), key=lambda item: len(item[0])):\n                base, *_ = version.partition(\"+\")\n                if candidate.version in (version, base):\n                    return version, data.get(\"dependencies\", [])\n        except KeyError:\n            pass\n        assert candidate.prepared is not None\n        meta = candidate.prepared.metadata\n        return meta.version, meta.requires or []\n\n\nclass TestRepository(BaseRepository):\n    \"\"\"\n    A mock repository to ease testing dependencies\n    \"\"\"\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self._pypi_data = self.get_data()\n\n    def get_data(self) -> dict[str, Any]:\n        raise NotImplementedError(\"To be injected by the fixture.\")\n\n    def _get_dependencies_from_fixture(self, candidate: Candidate) -> CandidateMetadata:\n        try:\n            pypi_data = self._pypi_data[cast(str, candidate.req.key)][cast(str, candidate.version)]\n        except KeyError:\n            raise CandidateInfoNotFound(candidate) from None\n        deps = pypi_data.get(\"dependencies\", [])\n        deps = filter_requirements_with_extras(deps, candidate.req.extras or ())\n        return CandidateMetadata(deps, pypi_data.get(\"requires_python\", \"\"), \"\")\n\n    def dependency_generators(self) -> Iterable[Callable[[Candidate], CandidateMetadata]]:\n        return (\n            self._get_dependencies_from_cache,\n            self._get_dependencies_from_fixture,\n            self._get_dependencies_from_metadata,\n        )\n\n    def get_hashes(self, candidate: Candidate) -> list[FileHash]:\n        return []\n\n    def _find_candidates(self, requirement: Requirement, minimal_version: bool) -> Iterable[Candidate]:\n        for version, candidate in sorted(\n            self._pypi_data.get(cast(str, requirement.key), {}).items(),\n            key=lambda item: parse_version(item[0]),\n            reverse=not minimal_version,\n        ):\n            c = Candidate(\n                requirement,\n                name=requirement.project_name,\n                version=version,\n            )\n            c.requires_python = candidate.get(\"requires_python\", \"\")\n            c.link = Link(f\"https://mypypi.org/packages/{c.name}-{c.version}.tar.gz\")\n            yield c\n\n\nclass Metadata(dict):\n    def get_all(self, name: str, fallback: list[str] | None = None) -> list[str] | None:\n        return [self[name]] if name in self else fallback\n\n    def __getitem__(self, __key: str) -> str:\n        return cast(str, dict.get(self, __key))\n\n\nclass Distribution:\n    \"\"\"A mock Distribution\"\"\"\n\n    def __init__(\n        self,\n        key: str,\n        version: str,\n        editable: bool = False,\n        metadata: Metadata | None = None,\n    ):\n        self.version = version\n        self.link_file = \"editable\" if editable else None\n        self.dependencies: list[str] = []\n        self._metadata = {\"Name\": key, \"Version\": version}\n        if metadata:\n            self._metadata.update(metadata)\n        self.name = key\n\n    @property\n    def metadata(self) -> Metadata:\n        return Metadata(self._metadata)\n\n    def as_req(self) -> Requirement:\n        return parse_requirement(f\"{self.name}=={self.version}\")\n\n    @property\n    def requires(self) -> list[str]:\n        return self.dependencies\n\n    def read_text(self, path: Path | str) -> None:\n        return None\n\n\nclass MockWorkingSet(collections.abc.MutableMapping):\n    \"\"\"A mock working set\"\"\"\n\n    _data: dict[str, Distribution]\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        self._data = {}\n\n    def add_distribution(self, dist: Distribution) -> None:\n        self._data[dist.name] = dist\n\n    def is_owned(self, key: str) -> bool:\n        return key in self._data\n\n    def __getitem__(self, key: str) -> Distribution:\n        return self._data[key]\n\n    def __len__(self) -> int:\n        return len(self._data)\n\n    def __iter__(self) -> Iterator[str]:\n        return iter(self._data)\n\n    def __setitem__(self, key: str, value: Distribution) -> None:\n        self._data[key] = value\n\n    def __delitem__(self, key: str) -> None:\n        del self._data[key]\n\n\n# Note:\n#   When going through pytest assertions rewrite, the future annotations is ignored.\n#   As a consequence, type definition must comply with Python 3.7 syntax\n\nIndexMap = dict[str, Path]\n\"\"\"Path some root-relative http paths to some local paths\"\"\"\nIndexOverrides = dict[str, bytes]\n\"\"\"PyPI indexes overrides fixture format\"\"\"\nIndexesDefinition = dict[str, Union[tuple[IndexMap, IndexOverrides, bool], IndexMap]]\n\"\"\"Mock PyPI indexes format\"\"\"\n\n\n@pytest.fixture(scope=\"session\")\ndef build_env_wheels() -> Iterable[Path]:\n    \"\"\"\n    Expose some wheels to be installed in the build environment.\n\n    Override to provide your owns.\n\n    Returns:\n        a list of wheels paths to install\n    \"\"\"\n    return []\n\n\n@pytest.fixture(autouse=True)\ndef temp_env(monkeypatch: pytest.MonkeyPatch) -> Generator[MutableMapping[str, str]]:\n    old_env = os.environ.copy()\n    monkeypatch.setattr(\"pdm.models.candidates.PreparedCandidate._build_dir_cache\", {})\n    try:\n        yield os.environ\n    finally:\n        os.environ.clear()\n        os.environ.update(old_env)\n\n\n@pytest.fixture(scope=\"session\")\ndef build_env(build_env_wheels: Iterable[Path], tmp_path_factory: pytest.TempPathFactory) -> Path:\n    \"\"\"\n    A fixture build environment\n\n    Args:\n        build_env_wheels: a list of wheel to install in the environment\n\n    Returns:\n        The build environment temporary path\n    \"\"\"\n    d = tmp_path_factory.mktemp(\"pdm-test-env\")\n    p = Core().create_project(d)\n    env = PythonEnvironment(p, prefix=str(d), python=sys.executable)\n    for wheel in build_env_wheels:\n        install_wheel(wheel, env, requested=True)\n    return d\n\n\n@pytest.fixture\ndef pypi_indexes() -> IndexesDefinition:\n    \"\"\"\n    Provides some mocked PyPI entries\n\n    Returns:\n        a definition of the mocked indexes\n    \"\"\"\n    return {}\n\n\n_build_session = BaseEnvironment._build_session\n\n\n@pytest.fixture\ndef build_test_session(pypi_indexes: IndexesDefinition) -> Callable[..., PDMPyPIClient]:\n    def get_pypi_session(*args: Any, **kwargs: Any) -> PDMPyPIClient:\n        mounts: dict[str, httpx.BaseTransport] = {}\n        for root, specs in pypi_indexes.items():\n            index, overrides, strip = specs if isinstance(specs, tuple) else (specs, None, False)\n            mounts[root] = LocalIndexTransport(index, overrides=overrides, strip_suffix=strip)\n        kwargs[\"mounts\"] = mounts\n        return _build_session(*args, **kwargs)\n\n    return get_pypi_session\n\n\ndef remove_pep582_path_from_pythonpath(pythonpath: str) -> str:\n    \"\"\"Remove all pep582 paths of PDM from PYTHONPATH\"\"\"\n    paths = pythonpath.split(os.pathsep)\n    paths = [path for path in paths if \"pdm/pep582\" not in path]\n    return os.pathsep.join(paths)\n\n\n@pytest.fixture\ndef core() -> Iterator[Core]:\n    old_config_map = Config._config_map.copy()\n    # Turn off use_venv by default, for testing\n    Config._config_map[\"python.use_venv\"].default = False\n    main = Core()\n    with main.exit_stack:\n        yield main\n    # Restore the config items\n    Config._config_map = old_config_map\n\n\n@pytest.fixture\ndef project_no_init(\n    tmp_path: Path,\n    mocker: MockerFixture,\n    core: Core,\n    build_test_session: Callable[..., PDMPyPIClient],\n    monkeypatch: pytest.MonkeyPatch,\n    build_env: Path,\n) -> Project:\n    \"\"\"\n    A fixture creating a non-initialized test project for the current test.\n\n    Returns:\n        The non-initialized project\n    \"\"\"\n    test_home = tmp_path / \".pdm-home\"\n    test_home.mkdir(parents=True)\n    test_home.joinpath(\"config.toml\").write_text(\n        '[global_project]\\npath = \"{}\"\\n'.format(test_home.joinpath(\"global-project\").as_posix())\n    )\n    p = core.create_project(tmp_path, global_config=test_home.joinpath(\"config.toml\").as_posix())\n    p.global_config[\"python.install_root\"] = str(tmp_path / \"pythons\")\n    p.global_config[\"venv.location\"] = str(tmp_path / \"venvs\")\n    mocker.patch.object(BaseEnvironment, \"_build_session\", build_test_session)\n    mocker.patch(\"pdm.builders.base.EnvBuilder.get_shared_env\", return_value=str(build_env))\n    tmp_path.joinpath(\"caches\").mkdir(parents=True)\n    p.global_config[\"cache_dir\"] = tmp_path.joinpath(\"caches\").as_posix()\n    p.global_config[\"log_dir\"] = tmp_path.joinpath(\"logs\").as_posix()\n    python_path = Path(sys.executable)\n    p._saved_python = python_path.as_posix()\n    monkeypatch.delenv(\"VIRTUAL_ENV\", raising=False)\n    monkeypatch.delenv(\"CONDA_PREFIX\", raising=False)\n    monkeypatch.delenv(\"PEP582_PACKAGES\", raising=False)\n    monkeypatch.delenv(\"NO_SITE_PACKAGES\", raising=False)\n    pythonpath = os.getenv(\"PYTHONPATH\", \"\")\n    pythonpath = remove_pep582_path_from_pythonpath(pythonpath)\n    p.pyproject.open_for_write()\n    if pythonpath:\n        monkeypatch.setenv(\"PYTHONPATH\", pythonpath)\n    return p\n\n\n@pytest.fixture\ndef project(project_no_init: Project) -> Project:\n    \"\"\"\n    A fixture creating an initialized test project for the current test.\n\n    Returns:\n        The initialized project\n    \"\"\"\n    from pdm.cli.utils import merge_dictionary\n\n    data = {\n        \"project\": {\n            \"name\": \"test-project\",\n            \"version\": \"0.0.0\",\n            \"description\": \"\",\n            \"authors\": [],\n            \"license\": {\"text\": \"MIT\"},\n            \"dependencies\": [],\n            \"requires-python\": \">=3.7\",\n        },\n        \"build-system\": DEFAULT_BACKEND.build_system(),\n    }\n\n    merge_dictionary(project_no_init.pyproject.open_for_write(), data)\n    project_no_init.pyproject.write()\n    # Clean the cached property\n    project_no_init._environment = None\n    return project_no_init\n\n\n@pytest.fixture\ndef working_set(mocker: MockerFixture, repository: RepositoryData) -> MockWorkingSet:\n    \"\"\"\n    a mock working set as a fixture\n\n    Returns:\n        a mock working set\n    \"\"\"\n    from pdm.installers import InstallManager\n\n    rv = MockWorkingSet()\n    mocker.patch.object(BaseEnvironment, \"get_working_set\", return_value=rv)\n\n    class MockInstallManager(InstallManager):\n        def install(self, candidate: Candidate) -> Distribution:  # type: ignore[override]\n            key = normalize_name(candidate.name or \"\")\n            candidate.prepare(self.environment)\n            version, dependencies = repository.get_raw_dependencies(candidate)\n            dist = Distribution(key, version, candidate.req.editable)\n            dist.dependencies = dependencies\n            rv.add_distribution(dist)\n            return dist\n\n        def uninstall(self, dist: Distribution) -> None:  # type: ignore[override]\n            del rv[dist.name]\n\n        def overwrite(self, dist: Distribution, candidate: Candidate) -> None:  # type: ignore[override]\n            self.uninstall(dist)\n            self.install(candidate)\n\n    mocker.patch.object(Core, \"install_manager_class\", MockInstallManager)\n\n    return rv\n\n\n@pytest.fixture\ndef local_finder_artifacts() -> Path:\n    \"\"\"\n    The local finder search path as a fixture\n\n    Override to provides your own artifacts.\n\n    Returns:\n        The path to the artifacts root\n    \"\"\"\n    return Path()\n\n\n@pytest.fixture\ndef local_finder(project_no_init: Project, local_finder_artifacts: Path) -> None:\n    project_no_init.pyproject.settings[\"source\"] = [\n        {\n            \"type\": \"find_links\",\n            \"verify_ssl\": False,\n            \"url\": local_finder_artifacts.as_uri(),\n            \"name\": \"pypi\",\n        }\n    ]\n    project_no_init.pyproject.write()\n\n\n@pytest.fixture\ndef repository_pypi_json() -> Path:\n    \"\"\"\n    The test repository fake PyPI definition path as a fixture\n\n    Override to provides your own definition path.\n\n    Returns:\n        The path to a fake PyPI repository JSON definition\n    \"\"\"\n    return Path()\n\n\n@pytest.fixture()\ndef repository(\n    core: Core,\n    mocker: MockerFixture,\n    repository_pypi_json: Path,\n    local_finder: type[None],\n) -> RepositoryData:\n    \"\"\"\n    A fixture providing a mock PyPI repository\n\n    Returns:\n        A mock repository\n    \"\"\"\n    repo = RepositoryData(repository_pypi_json)\n    core.repository_class = TestRepository\n    mocker.patch.object(TestRepository, \"get_data\", return_value=repo.pypi_data)\n    return repo\n\n\n@dataclass\nclass RunResult:\n    \"\"\"\n    Store a command execution result.\n    \"\"\"\n\n    exit_code: int\n    \"\"\"The execution exit code\"\"\"\n    stdout: str\n    \"\"\"The execution `stdout` output\"\"\"\n    stderr: str\n    \"\"\"The execution `stderr` output\"\"\"\n    exception: Exception | None = None\n    \"\"\"If set, the exception raised on execution\"\"\"\n\n    @property\n    def output(self) -> str:\n        \"\"\"The execution `stdout` output (`stdout` alias)\"\"\"\n        return self.stdout\n\n    @property\n    def outputs(self) -> str:\n        \"\"\"The execution `stdout` and `stderr` outputs concatenated\"\"\"\n        return self.stdout + self.stderr\n\n    def print(self) -> None:\n        \"\"\"A debugging facility\"\"\"\n        print(\"# exit code:\", self.exit_code)\n        print(\"# stdout:\", self.stdout, sep=\"\\n\")\n        print(\"# stderr:\", self.stderr, sep=\"\\n\")\n\n\nif TYPE_CHECKING:\n\n    class PDMCallable(Protocol):\n        \"\"\"The PDM fixture callable signature\"\"\"\n\n        def __call__(\n            self,\n            args: str | list[str],\n            strict: bool = False,\n            input: str | None = None,\n            obj: Project | None = None,\n            env: Mapping[str, str] | None = None,\n            cleanup: bool = True,\n            **kwargs: Any,\n        ) -> RunResult:\n            \"\"\"\n            Args:\n                args: the command arguments as a single lexable string or a strings array\n                strict: raise an exception on failure instead of returning if enabled\n                input: an optional string to be submitted too `stdin`\n                obj: an optional existing `Project`.\n                env: override the environment variables with those\n\n            Returns:\n                The command result\n            \"\"\"\n            ...\n\n\n@pytest.fixture\ndef pdm(core: Core, monkeypatch: pytest.MonkeyPatch) -> PDMCallable:\n    \"\"\"\n    A fixture allowing to execute PDM commands\n\n    Returns:\n        A `pdm` fixture command.\n    \"\"\"\n    # Hide the spinner text from testing output to not break existing tests\n    monkeypatch.setattr(\"pdm.termui.DummySpinner._show\", lambda self: None)\n\n    def caller(\n        args: str | list[str],\n        strict: bool = False,\n        input: str | None = None,\n        obj: Project | None = None,\n        env: Mapping[str, str] | None = None,\n        cleanup: bool = True,\n        **kwargs: Any,\n    ) -> RunResult:\n        __tracebackhide__ = True\n\n        stdin = StringIO(input)\n        stdout = StringIO()\n        stderr = StringIO()\n        exit_code: int = 0\n        exception: Exception | None = None\n        args = args.split() if isinstance(args, str) else args\n\n        with monkeypatch.context() as m:\n            old_env = os.environ.copy()\n            m.setattr(\"sys.stdin\", stdin)\n            m.setattr(\"sys.stdout\", stdout)\n            m.setattr(\"sys.stderr\", stderr)\n            for key, value in (env or {}).items():\n                m.setenv(key, value)\n            try:\n                core.main(args, \"pdm\", obj=obj, **kwargs)\n            except SystemExit as e:\n                exit_code = cast(int, e.code)\n            except Exception as e:\n                exit_code = 1\n                exception = e\n            finally:\n                os.environ.clear()\n                os.environ.update(old_env)\n                if cleanup:\n                    core.exit_stack.close()\n                    # Clear the build directory cache to avoid stale references\n                    from pdm.models.candidates import PreparedCandidate\n\n                    PreparedCandidate._build_dir_cache.clear()\n\n        result = RunResult(exit_code, stdout.getvalue(), stderr.getvalue(), exception)\n\n        if strict and result.exit_code != 0:\n            if result.exception:\n                raise result.exception.with_traceback(result.exception.__traceback__)\n            raise RuntimeError(f\"Call command {args} failed({result.exit_code}): {result.stderr}\")\n        return result\n\n    return caller\n\n\nVENV_BACKENDS = [\"virtualenv\", \"venv\"]\n\n\n@pytest.fixture(params=VENV_BACKENDS)\ndef venv_backends(project: Project, request: SubRequest) -> None:\n    \"\"\"A fixture iterating over `venv` backends\"\"\"\n    project.project_config[\"venv.backend\"] = request.param\n    project.project_config[\"venv.prompt\"] = \"{project_name}-{python_version}\"\n    project.project_config[\"python.use_venv\"] = True\n    shutil.rmtree(project.root / \"__pypackages__\", ignore_errors=True)\n"
  },
  {
    "path": "src/pdm/resolver/__init__.py",
    "content": "from .base import Resolver\nfrom .resolvelib import RLResolver\nfrom .uv import UvResolver\n\n__all__ = [\"RLResolver\", \"Resolver\", \"UvResolver\"]\n"
  },
  {
    "path": "src/pdm/resolver/base.py",
    "content": "from __future__ import annotations\n\nimport abc\nimport typing as t\nfrom dataclasses import dataclass, field\n\nfrom resolvelib import BaseReporter\n\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.repositories import LockedRepository\n\nif t.TYPE_CHECKING:\n    from pdm.environments import BaseEnvironment\n    from pdm.models.markers import EnvSpec\n    from pdm.models.repositories import Package\n    from pdm.models.requirements import Requirement\n    from pdm.project import Project\n\n\nclass Resolution(t.NamedTuple):\n    \"\"\"The resolution result.\"\"\"\n\n    packages: t.Iterable[Package]\n    \"\"\"The list of pinned packages with dependencies.\"\"\"\n    collected_groups: set[str]\n    \"\"\"The list of collected groups.\"\"\"\n\n    @property\n    def candidates(self) -> dict[str, Candidate]:\n        return {entry.candidate.identify(): entry.candidate for entry in self.packages}\n\n\n@dataclass\nclass Resolver(abc.ABC):\n    \"\"\"The resolver class.\"\"\"\n\n    environment: BaseEnvironment\n    \"\"\"The environment instance.\"\"\"\n    requirements: list[Requirement]\n    \"\"\"The list of requirements to resolve.\"\"\"\n    update_strategy: str\n    \"\"\"The update strategy to use [all|reuse|eager|reuse-installed].\"\"\"\n    strategies: set[str]\n    \"\"\"The list of strategies to use.\"\"\"\n    target: EnvSpec\n    \"\"\"The target environment specification.\"\"\"\n    tracked_names: t.Collection[str] = ()\n    \"\"\"The list of tracked names.\"\"\"\n    keep_self: bool = False\n    \"\"\"Whether to keep self dependencies.\"\"\"\n    locked_repository: LockedRepository | None = None\n    \"\"\"The repository with all locked dependencies.\"\"\"\n    reporter: BaseReporter = field(default_factory=BaseReporter)\n    \"\"\"The reporter to use.\"\"\"\n    requested_groups: set[str] = field(default_factory=set, init=False)\n    \"\"\"The list of requested groups.\"\"\"\n\n    def __post_init__(self) -> None:\n        self.requested_groups = {g for r in self.requirements for g in r.groups}\n\n    @abc.abstractmethod\n    def resolve(self) -> Resolution:\n        \"\"\"Resolve the requirements.\"\"\"\n        pass\n\n    @property\n    def project(self) -> Project:\n        \"\"\"The project instance.\"\"\"\n        return self.environment.project\n"
  },
  {
    "path": "src/pdm/resolver/graph.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, AbstractSet, Iterable, Iterator, TypeVar, overload\n\nfrom pdm.models.markers import Marker, get_marker\n\nif TYPE_CHECKING:\n    from resolvelib.resolvers import Criterion, Result\n\n    from pdm.models.candidates import Candidate\n    from pdm.models.requirements import Requirement\n\nT = TypeVar(\"T\")\n\n\nclass OrderedSet(AbstractSet[T]):\n    \"\"\"Set with deterministic ordering.\"\"\"\n\n    __slots__ = \"_data\"\n\n    def __init__(self, iterable: Iterable[T] = ()) -> None:\n        self._data = dict.fromkeys(iterable)\n\n    def __hash__(self) -> int:\n        return self._hash()\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}({self})\"\n\n    def __str__(self) -> str:\n        return f\"{{{', '.join(map(repr, self._data))}}}\"\n\n    def __contains__(self, obj: object) -> bool:\n        return obj in self._data\n\n    def __iter__(self) -> Iterator[T]:\n        return iter(self._data)\n\n    def __len__(self) -> int:\n        return len(self._data)\n\n\n@overload\ndef _identify_parent(parent: None) -> None: ...\n\n\n@overload\ndef _identify_parent(parent: Candidate) -> str: ...\n\n\ndef _identify_parent(parent: Candidate | None) -> str | None:\n    return parent.identify() if parent else None\n\n\ndef merge_markers(result: Result[Requirement, Candidate, str]) -> dict[str, Marker]:\n    \"\"\"Traverse through the parent dependencies till the top\n    and merge any requirement markers on the path.\n    Return a map of Metaset for each candidate.\n    \"\"\"\n    all_markers: dict[str, Marker] = {}\n    unresolved = OrderedSet(result.mapping)\n    circular: dict[str, OrderedSet[str]] = {}\n\n    while unresolved:\n        new_markers: dict[str, Marker] = {}\n        for k in unresolved:\n            crit = result.criteria[k]\n            keep_unresolved = circular.get(k) or OrderedSet()\n            # All parents must be resolved first\n            if any(p and _identify_parent(p) in (unresolved - keep_unresolved) for p in crit.iter_parent()):\n                continue\n            new_markers[k] = _build_marker(crit, all_markers, keep_unresolved)\n\n        if new_markers:\n            all_markers.update(new_markers)\n            unresolved -= new_markers  # type: ignore[assignment,operator]\n        else:\n            # No progress, there are likely circular dependencies.\n            # Pick one package and keep its parents unresolved now, we will get into it\n            # after all others are resolved.\n            package = next((p for p in unresolved if p not in circular), None)\n            if not package:\n                break\n            crit = result.criteria[package]\n            unresolved_parents = OrderedSet(\n                filter(\n                    lambda p: p in unresolved and p != package,\n                    (_identify_parent(p) for p in crit.iter_parent() if p),\n                )\n            )\n            circular[package] = unresolved_parents\n\n    for key in circular:\n        crit = result.criteria[key]\n        all_markers[key] = _build_marker(crit, all_markers, set())\n\n    return all_markers\n\n\ndef _build_marker(\n    crit: Criterion[Requirement, Candidate, str], resolved: dict[str, Marker], keep_unresolved: AbstractSet[str]\n) -> Marker:\n    marker = None\n\n    for r, parent in crit.information:\n        if parent and ((k := _identify_parent(parent)) in keep_unresolved or k not in resolved):\n            continue\n        this_marker = r.marker if r.marker is not None else get_marker(\"\")\n        # Use 'and' to connect markers inherited from parent.\n        if not parent:\n            parent_marker = get_marker(\"\")\n        else:\n            parent_marker = resolved[_identify_parent(parent)]\n        merged = this_marker & parent_marker\n        # Use 'or' to connect metasets inherited from different parents.\n        marker = (marker | merged) if marker is not None else merged  # type: ignore[operator]\n    return marker if marker is not None else get_marker(\"\")\n\n\ndef populate_groups(result: Result[Requirement, Candidate, str]) -> None:\n    \"\"\"Find where the candidates come from by traversing\n    the dependency tree back to the top.\n    \"\"\"\n\n    resolved: dict[str, set[str]] = {}\n\n    def get_candidate_groups(key: str) -> set[str]:\n        if key in resolved:\n            return resolved[key]\n        res = resolved[key] = set()\n        crit = result.criteria[key]\n        for req, parent in crit.information:\n            res.update(req.groups)\n            if parent is not None:\n                pkey = _identify_parent(parent)\n                res.update(get_candidate_groups(pkey))\n        return res\n\n    for k, can in result.mapping.items():\n        can.req.groups = sorted(get_candidate_groups(k))\n"
  },
  {
    "path": "src/pdm/resolver/providers.py",
    "content": "from __future__ import annotations\n\nimport dataclasses\nimport os\nfrom functools import cached_property\nfrom typing import TYPE_CHECKING, Callable\n\nfrom packaging.specifiers import InvalidSpecifier\nfrom packaging.version import InvalidVersion\nfrom resolvelib import AbstractProvider, RequirementsConflicted\nfrom resolvelib.resolvers import Criterion\n\nfrom pdm.exceptions import CandidateNotFound, InvalidPyVersion, RequirementError\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.repositories import LockedRepository\nfrom pdm.models.requirements import FileRequirement, Requirement, VcsRequirement, parse_requirement, strip_extras\nfrom pdm.models.specifiers import PySpecSet\nfrom pdm.resolver.python import PythonCandidate, PythonRequirement, find_python_matches, is_python_satisfied_by\nfrom pdm.termui import logger\nfrom pdm.utils import (\n    deprecation_warning,\n    get_requirement_from_override,\n    normalize_name,\n    parse_version,\n    url_without_fragments,\n)\n\nif TYPE_CHECKING:\n    from typing import Iterable, Iterator, Mapping, Sequence, TypeVar\n\n    from resolvelib.resolvers import RequirementInformation\n\n    from pdm._types import Comparable\n    from pdm.models.repositories import BaseRepository, LockedRepository\n    from pdm.models.requirements import Requirement\n    from pdm.models.working_set import WorkingSet\n\n    ProviderT = TypeVar(\"ProviderT\", bound=\"type[BaseProvider]\")\n\n\n_PROVIDER_REGISTRY: dict[str, type[BaseProvider]] = {}\n\n\ndef get_provider(strategy: str) -> type[BaseProvider]:\n    return _PROVIDER_REGISTRY[strategy]\n\n\ndef register_provider(strategy: str) -> Callable[[ProviderT], ProviderT]:\n    def wrapper(cls: ProviderT) -> ProviderT:\n        _PROVIDER_REGISTRY[strategy] = cls\n        return cls\n\n    return wrapper\n\n\n@register_provider(\"all\")\nclass BaseProvider(AbstractProvider[Requirement, Candidate, str]):\n    def __init__(\n        self,\n        repository: BaseRepository,\n        allow_prereleases: bool | None = None,\n        overrides: dict[str, str] | None = None,\n        direct_minimal_versions: bool = False,\n        *,\n        locked_repository: LockedRepository | None = None,\n    ) -> None:\n        if overrides is not None:  # pragma: no cover\n            deprecation_warning(\n                \"The `overrides` argument is deprecated and will be removed in the future.\", stacklevel=2\n            )\n        if allow_prereleases is not None:  # pragma: no cover\n            deprecation_warning(\n                \"The `allow_prereleases` argument is deprecated and will be removed in the future.\", stacklevel=2\n            )\n        project = repository.environment.project\n        self.repository = repository\n        self.allow_prereleases = project.pyproject.allow_prereleases  # Root allow_prereleases value\n        self.fetched_dependencies: dict[tuple[str, str | None], list[Requirement]] = {}\n        self.excludes = {normalize_name(k) for k in project.pyproject.resolution.get(\"excludes\", [])}\n        self.direct_minimal_versions = direct_minimal_versions\n        self.locked_repository = locked_repository\n\n    def requirement_preference(self, requirement: Requirement) -> Comparable:\n        \"\"\"Return the preference of a requirement to find candidates.\n\n        - Editable requirements are preferred.\n        - File links are preferred.\n        - The one with narrower specifierset is preferred.\n        \"\"\"\n        editable = requirement.editable\n        is_named = requirement.is_named\n        is_pinned = requirement.is_pinned\n        is_prerelease = bool(requirement.prerelease) or bool(requirement.specifier.prereleases)\n        specifier_parts = len(requirement.specifier)\n        return (not editable, is_named, not is_pinned, not is_prerelease, -specifier_parts)\n\n    def identify(self, requirement_or_candidate: Requirement | Candidate) -> str:\n        return requirement_or_candidate.identify()\n\n    def get_preference(\n        self,\n        identifier: str,\n        resolutions: dict[str, Candidate],\n        candidates: dict[str, Iterator[Candidate]],\n        information: dict[str, Iterator[RequirementInformation]],\n        backtrack_causes: Sequence[RequirementInformation],\n    ) -> tuple[Comparable, ...]:\n        is_top = any(parent is None for _, parent in information[identifier])\n        backtrack_identifiers = {req.identify() for req, _ in backtrack_causes} | {\n            parent.identify() for _, parent in backtrack_causes if parent is not None\n        }\n        # Use the REAL identifier as it may be updated after candidate preparation.\n        deps: list[Requirement] = []\n        for candidate in candidates[identifier]:\n            try:\n                deps = self.get_dependencies(candidate)\n            except RequirementsConflicted:\n                continue\n            break\n        is_backtrack_cause = any(dep.identify() in backtrack_identifiers for dep in deps)\n        is_file_or_url = any(not requirement.is_named for requirement, _ in information[identifier])\n        operators = [spec.operator for req, _ in information[identifier] for spec in req.specifier]\n        is_python = identifier == \"python\"\n        is_pinned = any(op[:2] == \"==\" for op in operators)\n        constraints = len(operators)\n        return (\n            not is_python,\n            not is_top,\n            not is_file_or_url,\n            not is_pinned,\n            not is_backtrack_cause,\n            -constraints,\n            identifier,\n        )\n\n    @cached_property\n    def locked_candidates(self) -> dict[str, list[Candidate]]:\n        return self.locked_repository.all_candidates if self.locked_repository else {}\n\n    @cached_property\n    def overrides(self) -> dict[str, Requirement]:\n        \"\"\"A mapping of package name to the requirement for overriding.\"\"\"\n        from pdm.formats.requirements import RequirementParser\n\n        project_overrides: dict[str, str] = {\n            normalize_name(k): v\n            for k, v in self.repository.environment.project.pyproject.resolution.get(\"overrides\", {}).items()\n        }\n        requirements: dict[str, Requirement] = {}\n        for name, value in project_overrides.items():\n            req = get_requirement_from_override(name, value)\n            r = parse_requirement(req)\n            requirements[r.identify()] = r\n\n        # Read from --override files\n        parser = RequirementParser(self.repository.environment.session)\n        for override_file in self.repository.environment.project.core.state.overrides:\n            parser.parse_file(override_file)\n        for r in parser.requirements:\n            # There might be duplicates, we only keep the last one\n            requirements[r.identify()] = r\n\n        return requirements\n\n    def _is_direct_requirement(self, requirement: Requirement) -> bool:\n        from itertools import chain\n\n        project = self.repository.environment.project\n        all_dependencies = chain.from_iterable(project.all_dependencies.values())\n        return any(r.is_named and requirement.identify() == r.identify() for r in all_dependencies)\n\n    def _find_candidates(self, requirement: Requirement) -> Iterable[Candidate]:\n        if not requirement.is_named and not isinstance(self.repository, LockedRepository):\n            can = Candidate(requirement)\n            if not can.name:\n                can.prepare(self.repository.environment).metadata\n            yield can\n        else:\n            prerelease = requirement.prerelease\n            if prerelease is None and requirement.is_pinned and requirement.specifier.prereleases:\n                prerelease = True\n            if prerelease is None and (key := requirement.identify()) in self.locked_candidates:\n                # keep the prerelease if it is locked\n                candidates = self.locked_candidates[key]\n                for candidate in candidates:\n                    if candidate.version is not None:\n                        try:\n                            parsed_version = parse_version(candidate.version)\n                        except InvalidVersion:  # pragma: no cover\n                            pass\n                        else:\n                            if parsed_version.is_prerelease:\n                                prerelease = True\n                                break\n            found = self.repository.find_candidates(\n                requirement,\n                self.allow_prereleases if prerelease is None else prerelease,\n                minimal_version=self.direct_minimal_versions and self._is_direct_requirement(requirement),\n            )\n\n            current_version: str | None = None\n            collected_wheels: list[Candidate] = []\n            collected_others: list[Candidate] = []\n            for candidate in found:\n                assert candidate.version is not None\n                if current_version is None:\n                    current_version = candidate.version\n                if candidate.version != current_version:\n                    # If there are wheels for the given version, we should only return wheels\n                    # to avoid build steps.\n                    if collected_wheels:\n                        yield collected_wheels[0]\n                    elif collected_others:\n                        yield collected_others[0]\n                    current_version = candidate.version\n                    collected_wheels.clear()\n                    collected_others.clear()\n                if candidate.link and candidate.link.is_wheel:\n                    collected_wheels.append(candidate)\n                else:\n                    collected_others.append(candidate)\n            if collected_wheels:\n                yield collected_wheels[0]\n            elif collected_others:\n                yield collected_others[0]\n\n    def find_matches(\n        self,\n        identifier: str,\n        requirements: Mapping[str, Iterator[Requirement]],\n        incompatibilities: Mapping[str, Iterator[Candidate]],\n    ) -> Callable[[], Iterator[Candidate]]:\n        def matches_gen() -> Iterator[Candidate]:\n            incompat = list(incompatibilities[identifier])\n            if identifier == \"python\":\n                candidates = find_python_matches(identifier, requirements)\n                return (c for c in candidates if c not in incompat)\n            elif identifier in self.overrides:\n                return iter(self._find_candidates(self.overrides[identifier]))\n            else:\n                name, extras = strip_extras(identifier)\n                if name in self.overrides:\n                    req = dataclasses.replace(self.overrides[name], extras=extras)\n                    return iter(self._find_candidates(req))\n            reqs = list(requirements[identifier])\n            if not reqs:\n                return iter(())\n            original_req = min(reqs, key=self.requirement_preference)\n            bare_name, extras = strip_extras(identifier)\n            if extras and bare_name in requirements:\n                # We should consider the requirements for both foo and foo[extra]\n                reqs.extend(requirements[bare_name])\n            reqs.sort(key=self.requirement_preference)\n            candidates = self._find_candidates(reqs[0])\n            return (\n                # In some cases we will use candidates from the bare requirement,\n                # this will miss the extra dependencies if any. So we associate the original\n                # requirement back with the candidate since it is used by `get_dependencies()`.\n                can.copy_with(original_req) if extras else can\n                for can in candidates\n                if can not in incompat and all(self.is_satisfied_by(r, can) for r in reqs)\n            )\n\n        return matches_gen\n\n    def _compare_file_reqs(self, req1: FileRequirement, req2: FileRequirement) -> bool:\n        backend = self.repository.environment.project.backend\n        if req1.path and req2.path:\n            return os.path.normpath(req1.path.absolute()) == os.path.normpath(req2.path.absolute())\n        left = backend.expand_line(url_without_fragments(req1.get_full_url()))\n        right = backend.expand_line(url_without_fragments(req2.get_full_url()))\n        return left == right\n\n    def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> bool:\n        if isinstance(requirement, PythonRequirement):\n            return is_python_satisfied_by(requirement, candidate)\n        elif (name := candidate.identify()) in self.overrides or strip_extras(name)[0] in self.overrides:\n            return True\n        if not requirement.is_named:\n            if candidate.req.is_named:\n                return False\n            can_req = candidate.req\n            if requirement.is_vcs and can_req.is_vcs:\n                return can_req.vcs == requirement.vcs and can_req.repo == requirement.repo  # type: ignore[attr-defined]\n            return self._compare_file_reqs(requirement, can_req)  # type: ignore[arg-type]\n        version = candidate.version\n        this_name = self.repository.environment.project.name\n        if version is None or candidate.name == this_name:\n            # This should be a URL candidate or self package, consider it to be matching\n            return True\n        # Allow prereleases if: 1) it is not specified in the tool settings or\n        # 2) the candidate doesn't come from PyPI index or 3) the requirement is pinned\n        allow_prereleases = (\n            self.allow_prereleases in (True, None) or not candidate.req.is_named or requirement.is_pinned\n        )\n        return requirement.specifier.contains(version, allow_prereleases)\n\n    def _get_dependencies_from_repository(self, candidate: Candidate) -> tuple[list[Requirement], PySpecSet, str]:\n        return self.repository.get_dependencies(candidate)\n\n    def get_dependencies(self, candidate: Candidate) -> list[Requirement]:\n        if isinstance(candidate, PythonCandidate):\n            return []\n        try:\n            deps, requires_python, _ = self._get_dependencies_from_repository(candidate)\n        except (RequirementError, InvalidPyVersion, InvalidSpecifier) as e:\n            # When the metadata is invalid, skip this candidate by marking it as conflicting.\n            # Here we pass an empty criterion so it doesn't provide any info to the resolution.\n            logger.error(\"Invalid metadata in %s: %s\", candidate, e)\n            raise RequirementsConflicted(Criterion([], [], [])) from None\n\n        if candidate.req.extras:\n            # XXX: If the requirement has extras, add the original candidate\n            # (without extras) as its dependency. This ensures the same package with\n            # different extras resolve to the same version.\n            self_req = dataclasses.replace(\n                candidate.req.as_pinned_version(candidate.version),\n                extras=None,\n                marker=None,\n            )\n            if self_req not in deps:\n                deps.insert(0, self_req)\n        self.fetched_dependencies[candidate.dep_key] = deps[:]\n        # Filter out incompatible dependencies(e.g. functools32) early so that\n        # we don't get errors when building wheels.\n        valid_deps: list[Requirement] = []\n        for dep in deps:\n            if (\n                dep.requires_python\n                & requires_python\n                & candidate.req.requires_python\n                & PySpecSet(self.repository.env_spec.requires_python)\n            ).is_empty():\n                continue\n            if dep.marker and not dep.marker.matches(self.repository.env_spec):\n                continue\n            if dep.identify() in self.excludes:\n                continue\n            dep.requires_python &= candidate.req.requires_python\n            valid_deps.append(dep)\n        # A candidate contributes to the Python requirements only when:\n        # It isn't an optional dependency, or the requires-python doesn't cover\n        # the req's requires-python.\n        # For example, A v1 requires python>=3.6, it not eligible on a project with\n        # requires-python=\">=2.7\". But it is eligible if A has environment marker\n        # A1; python_version>='3.8'\n        new_requires_python = candidate.req.requires_python & self.repository.environment.python_requires\n        if not (\n            candidate.identify() in self.overrides\n            or new_requires_python.is_empty()\n            or requires_python.is_superset(new_requires_python)\n        ):\n            valid_deps.append(PythonRequirement.from_pyspec_set(requires_python))\n        return valid_deps\n\n\n@register_provider(\"reuse\")\nclass ReusePinProvider(BaseProvider):\n    \"\"\"A provider that reuses preferred pins if possible.\n\n    This is used to implement \"add\", \"remove\", and \"reuse upgrade\",\n    where already-pinned candidates in lockfile should be preferred.\n    \"\"\"\n\n    def __init__(\n        self,\n        repository: BaseRepository,\n        allow_prereleases: bool | None = None,\n        overrides: dict[str, str] | None = None,\n        direct_minimal_versions: bool = False,\n        *,\n        locked_repository: LockedRepository | None = None,\n        tracked_names: Iterable[str],\n    ) -> None:\n        super().__init__(\n            repository=repository,\n            allow_prereleases=allow_prereleases,\n            overrides=overrides,\n            direct_minimal_versions=direct_minimal_versions,\n            locked_repository=locked_repository,\n        )\n        self.tracked_names = set(tracked_names)\n\n    def iter_reuse_candidates(self, identifier: str, requirement: Requirement | None) -> Iterable[Candidate]:\n        bare_name = strip_extras(identifier)[0]\n        if bare_name in self.tracked_names or identifier not in self.locked_candidates:\n            return []\n        return sorted(self.locked_candidates[identifier], key=lambda c: c.version or \"\", reverse=True)\n\n    def get_reuse_candidate(self, identifier: str, requirement: Requirement | None) -> Candidate | None:\n        deprecation_warning(\n            \"The get_reuse_candidate method is deprecated, use iter_reuse_candidates instead.\", stacklevel=2\n        )\n        return next(iter(self.iter_reuse_candidates(identifier, requirement)), None)\n\n    def find_matches(\n        self,\n        identifier: str,\n        requirements: Mapping[str, Iterator[Requirement]],\n        incompatibilities: Mapping[str, Iterator[Candidate]],\n    ) -> Callable[[], Iterator[Candidate]]:\n        super_find = super().find_matches(identifier, requirements, incompatibilities)\n\n        def matches_gen() -> Iterator[Candidate]:\n            requested_req = next(filter(lambda r: r.is_named, requirements[identifier]), None)\n            for pin in self.iter_reuse_candidates(identifier, requested_req):\n                if identifier not in self.overrides and pin.req.is_named:\n                    pin = pin.copy_with(min(requirements[identifier], key=self.requirement_preference))\n                incompat = list(incompatibilities[identifier])\n                pin._preferred = True  # type: ignore[attr-defined]\n                if pin not in incompat and all(self.is_satisfied_by(r, pin) for r in requirements[identifier]):\n                    yield pin\n            yield from super_find()\n\n        return matches_gen\n\n    def _get_dependencies_from_repository(self, candidate: Candidate) -> tuple[list[Requirement], PySpecSet, str]:\n        is_stable_metadata = candidate.req.is_named or (\n            isinstance(candidate.req, VcsRequirement) and candidate.req.revision\n        )\n        if self.locked_repository is not None and is_stable_metadata:\n            try:\n                return self.locked_repository.get_dependencies(candidate)\n            except CandidateNotFound:\n                pass\n        return super()._get_dependencies_from_repository(candidate)\n\n\n@register_provider(\"eager\")\nclass EagerUpdateProvider(ReusePinProvider):\n    \"\"\"A specialized provider to handle an \"eager\" upgrade strategy.\n\n    An eager upgrade tries to upgrade not only packages specified, but also\n    their dependencies (recursively). This contrasts to the \"only-if-needed\"\n    default, which only promises to upgrade the specified package, and\n    prevents touching anything else if at all possible.\n\n    The provider is implemented as to keep track of all dependencies of the\n    specified packages to upgrade, and free their pins when it has a chance.\n    \"\"\"\n\n    def iter_reuse_candidates(self, identifier: str, requirement: Requirement | None) -> Iterable[Candidate]:\n        if identifier in self.tracked_names:\n            # If this is a tracked package, don't reuse its pinned version, so it can be upgraded.\n            return []\n        return super().iter_reuse_candidates(identifier, requirement)\n\n    def get_dependencies(self, candidate: Candidate) -> list[Requirement]:\n        # If this package is being tracked for upgrade, remove pins of its\n        # dependencies, and start tracking these new packages.\n        dependencies = super().get_dependencies(candidate)\n        if self.identify(candidate) in self.tracked_names:\n            for dependency in dependencies:\n                if dependency.key:\n                    self.tracked_names.add(dependency.key)\n        return dependencies\n\n    def get_preference(\n        self,\n        identifier: str,\n        resolutions: dict[str, Candidate],\n        candidates: dict[str, Iterator[Candidate]],\n        information: dict[str, Iterator[RequirementInformation]],\n        backtrack_causes: Sequence[RequirementInformation],\n    ) -> tuple[Comparable, ...]:\n        # Resolve tracking packages so we have a chance to unpin them first.\n        (python, *others) = super().get_preference(identifier, resolutions, candidates, information, backtrack_causes)\n        return (python, identifier not in self.tracked_names, *others)\n\n\n@register_provider(\"reuse-installed\")\nclass ReuseInstalledProvider(ReusePinProvider):\n    \"\"\"A provider that reuses installed packages if possible.\"\"\"\n\n    @cached_property\n    def installed(self) -> WorkingSet:\n        return self.repository.environment.get_working_set()\n\n    def iter_reuse_candidates(self, identifier: str, requirement: Requirement | None) -> Iterable[Candidate]:\n        key = strip_extras(identifier)[0]\n        if key not in self.installed or requirement is None:\n            return super().iter_reuse_candidates(identifier, requirement)\n        else:\n            dist = self.installed[key]\n            return [Candidate(requirement, installed=dist)]\n"
  },
  {
    "path": "src/pdm/resolver/python.py",
    "content": "\"\"\"\nSpecial requirement and candidate classes to describe a requires-python constraint\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Iterable, Iterator, Mapping, cast\n\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.requirements import NamedRequirement, Requirement\nfrom pdm.models.specifiers import PySpecSet\n\n\nclass PythonCandidate(Candidate):\n    def format(self) -> str:\n        return f\"[req]{self.name}[/][warning]{self.req.specifier!s}[/]\"\n\n\nclass PythonRequirement(NamedRequirement):\n    @classmethod\n    def from_pyspec_set(cls, spec: PySpecSet) -> PythonRequirement:\n        return cls(name=\"python\", specifier=spec)\n\n    def as_candidate(self) -> PythonCandidate:\n        return PythonCandidate(self)\n\n\ndef find_python_matches(\n    identifier: str,\n    requirements: Mapping[str, Iterator[Requirement]],\n) -> Iterable[Candidate]:\n    \"\"\"All requires-python except for the first one(must come from the project)\n    must be superset of the first one.\n    \"\"\"\n    python_reqs = cast(Iterator[PythonRequirement], iter(requirements[identifier]))\n    project_req = next(python_reqs)\n    python_specs = cast(Iterator[PySpecSet], (req.specifier for req in python_reqs))\n    if all(spec.is_superset(project_req.specifier or \"\") for spec in python_specs):\n        return [project_req.as_candidate()]\n    else:\n        # There is a conflict, no match is found.\n        return []\n\n\ndef is_python_satisfied_by(requirement: Requirement, candidate: Candidate) -> bool:\n    return cast(PySpecSet, requirement.specifier).is_superset(candidate.req.specifier)\n"
  },
  {
    "path": "src/pdm/resolver/reporters.py",
    "content": "from __future__ import annotations\n\nfrom contextlib import contextmanager\nfrom typing import TYPE_CHECKING, Any, Generator\n\nfrom resolvelib import BaseReporter\nfrom rich import get_console\nfrom rich.live import Live\nfrom rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TimeElapsedColumn\n\nfrom pdm.models.reporter import CandidateReporter, RichProgressReporter\nfrom pdm.termui import SPINNER, UI, Verbosity, logger\n\nif TYPE_CHECKING:\n    from resolvelib.resolvers import Criterion, RequirementInformation, State\n    from rich.console import Console, ConsoleOptions, RenderResult\n\n    from pdm.models.candidates import Candidate\n    from pdm.models.requirements import Requirement\n\n\ndef log_title(title: str) -> None:\n    logger.info(\"=\" * 8 + \" \" + title + \" \" + \"=\" * 8)\n\n\nclass LockReporter(BaseReporter):\n    @contextmanager\n    def make_candidate_reporter(self, candidate: Candidate) -> Generator[CandidateReporter]:\n        yield CandidateReporter()\n\n    def starting(self) -> Any:\n        log_title(\"Start resolving requirements\")\n\n    def adding_requirement(self, requirement: Requirement, parent: Candidate) -> None:\n        parent_line = f\"(from {parent.name} {parent.version})\" if parent else \"\"\n        logger.info(\"  Adding requirement %s%s\", requirement.as_line(), parent_line)\n\n    def ending(self, state: State) -> Any:\n        log_title(\"Resolution Result\")\n        if state.mapping:\n            column_width = max(map(len, state.mapping.keys()))\n            for k, can in state.mapping.items():\n                if not can.req.is_named:\n                    can_info = can.req.url\n                    if can.req.is_vcs:\n                        can_info = f\"{can_info}@{can.get_revision()}\"\n                else:\n                    can_info = can.version\n                logger.info(f\"  {k.rjust(column_width)} {can_info}\")\n\n\nclass RichLockReporter(LockReporter):\n    def __init__(self, requirements: list[Requirement], ui: UI) -> None:\n        self.ui = ui\n        self.console = get_console()\n        self.requirements = requirements\n        self.progress = Progress(\n            \"[progress.description]{task.description}\",\n            \"[info]{task.fields[text]}\",\n            BarColumn(),\n            TaskProgressColumn(),\n            console=self.console,\n        )\n        self._spinner = Progress(\n            SpinnerColumn(SPINNER, style=\"primary\"),\n            TimeElapsedColumn(),\n            \"[bold]{task.description}\",\n            \"{task.fields[info]}\",\n            console=self.console,\n        )\n        self._spinner_task = self._spinner.add_task(\"Resolving dependencies\", info=\"\", total=1)\n        self.live = Live(self)\n\n    @contextmanager\n    def make_candidate_reporter(self, candidate: Candidate) -> Generator[CandidateReporter]:\n        task_id = self.progress.add_task(f\"Resolving {candidate.format()}\", text=\"\", total=None)\n        try:\n            yield RichProgressReporter(self.progress, task_id)\n        finally:\n            self.progress.update(task_id, visible=False)\n            if candidate._prepared:\n                candidate._prepared.reporter = CandidateReporter()\n\n    def update(self, description: str | None = None, info: str | None = None, completed: float | None = None) -> None:\n        self._spinner.update(self._spinner_task, description=description, info=info, completed=completed)\n        self.live.refresh()\n\n    def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:  # pragma: no cover\n        yield self._spinner\n        yield self.progress\n\n    def start(self) -> None:\n        \"\"\"Start the progress display.\"\"\"\n        if self.ui.verbosity < Verbosity.DETAIL:\n            self.live.start(refresh=True)\n\n    def stop(self) -> None:\n        \"\"\"Stop the progress display.\"\"\"\n        self.live.stop()\n        if not self.console.is_interactive:  # pragma: no cover\n            self.console.print()\n\n    def __enter__(self) -> RichLockReporter:\n        self.start()\n        return self\n\n    def __exit__(self, *args: Any) -> None:\n        self.stop()\n\n    def starting_round(self, index: int) -> None:\n        log_title(f\"Starting round {index}\")\n\n    def starting(self) -> None:\n        \"\"\"Called before the resolution actually starts.\"\"\"\n        log_title(\"Start resolving requirements\")\n        for req in self.requirements:\n            logger.info(\"  \" + req.as_line())\n\n    def ending_round(self, index: int, state: State) -> None:\n        \"\"\"Called before each round of resolution ends.\n\n        This is NOT called if the resolution ends at this round. Use `ending`\n        if you want to report finalization. The index is zero-based.\n        \"\"\"\n        resolved = len(state.mapping)\n        to_resolve = len(state.criteria) - resolved\n        self.update(info=f\"[info]{resolved}[/] resolved, [info]{to_resolve}[/] to resolve\")\n\n    def rejecting_candidate(self, criterion: Criterion, candidate: Candidate) -> None:\n        if not criterion.information:\n            logger.info(\"Candidate rejected because it contains invalid metadata: %s\", candidate)\n            return\n        *others, last = criterion.information\n        logger.info(\n            \"Candidate rejected: %s because it introduces a new requirement %s\"\n            \" that conflicts with other requirements:\\n  %s\",\n            candidate,\n            last.requirement.as_line(),  # type: ignore[attr-defined]\n            \"  \\n\".join(\n                sorted({f\"  {req.as_line()} (from {parent if parent else 'project'})\" for req, parent in others})\n            ),\n        )\n\n    def pinning(self, candidate: Candidate) -> None:\n        \"\"\"Called when adding a candidate to the potential solution.\"\"\"\n        logger.info(\"Adding new pin: %s %s\", candidate.name, candidate.version)\n\n    def resolving_conflicts(self, causes: list[RequirementInformation]) -> None:\n        conflicts = sorted({f\"  {req.as_line()} (from {parent if parent else 'project'})\" for req, parent in causes})\n        logger.info(\"Conflicts detected: \\n%s\", \"\\n\".join(conflicts))\n"
  },
  {
    "path": "src/pdm/resolver/resolvelib.py",
    "content": "from __future__ import annotations\n\nimport inspect\nimport os\nfrom dataclasses import dataclass, replace\nfrom pathlib import Path\nfrom typing import cast\n\nfrom pdm import termui\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.markers import get_marker\nfrom pdm.models.requirements import FileRequirement, Requirement, strip_extras\nfrom pdm.models.specifiers import PySpecSet\nfrom pdm.project.lockfile import FLAG_DIRECT_MINIMAL_VERSIONS, FLAG_INHERIT_METADATA\nfrom pdm.resolver.base import Resolution, Resolver\nfrom pdm.resolver.graph import merge_markers, populate_groups\nfrom pdm.resolver.python import PythonRequirement\nfrom pdm.resolver.reporters import LockReporter, RichLockReporter\nfrom pdm.utils import normalize_name\n\n\n@dataclass\nclass RLResolver(Resolver):\n    def __post_init__(self) -> None:\n        super().__post_init__()\n        if self.locked_repository is None:\n            self.locked_repository = self.project.get_locked_repository()\n        supports_env_spec = \"env_spec\" in inspect.signature(self.project.get_provider).parameters\n        if supports_env_spec:\n            provider = self.project.get_provider(\n                self.update_strategy,\n                self.tracked_names,\n                direct_minimal_versions=FLAG_DIRECT_MINIMAL_VERSIONS in self.strategies,\n                env_spec=self.target,\n                locked_repository=self.locked_repository,\n            )\n        else:  # pragma: no cover\n            provider = self.project.get_provider(\n                self.update_strategy,\n                self.tracked_names,\n                direct_minimal_versions=FLAG_DIRECT_MINIMAL_VERSIONS in self.strategies,\n                ignore_compatibility=self.target.is_allow_all(),\n            )\n        if isinstance(self.reporter, LockReporter):\n            provider.repository.reporter = self.reporter\n        self.provider = provider\n\n    def resolve(self) -> Resolution:\n        from pdm.models.repositories import Package\n\n        mapping = self._do_resolve()\n        if self.project.enable_write_lockfile:  # type: ignore[has-type]\n            if isinstance(self.reporter, RichLockReporter):\n                self.reporter.update(info=\"Fetching hashes for resolved packages\")\n            self.provider.repository.fetch_hashes(mapping.values())\n        if not (env_python := PySpecSet(self.target.requires_python)).is_superset(self.environment.python_requires):\n            python_marker = get_marker(env_python.as_marker_string())\n            for candidate in mapping.values():\n                marker = candidate.req.marker or get_marker(\"\")\n                candidate.req = replace(candidate.req, marker=marker & python_marker)\n        backend = self.project.backend\n        packages: list[Package] = []\n        for candidate in mapping.values():\n            deps: list[str] = []\n            for r in self.provider.fetched_dependencies[candidate.dep_key]:\n                if isinstance(r, FileRequirement) and r.path:\n                    try:\n                        if r.path.is_absolute():\n                            r.path = Path(os.path.normpath(r.path)).relative_to(os.path.normpath(self.project.root))\n                    except ValueError:\n                        pass\n                    else:\n                        r.url = backend.relative_path_to_url(r.path.as_posix())\n                deps.append(r.as_line())\n            packages.append(Package(candidate, deps, candidate.summary))\n        return Resolution(packages, self.requested_groups)\n\n    def _do_resolve(self) -> dict[str, Candidate]:\n        from resolvelib import Resolver as _Resolver\n\n        resolver_class = cast(\"type[_Resolver]\", getattr(self.project.core, \"resolver_class\", _Resolver))\n        resolver = resolver_class(self.provider, self.reporter)\n        provider = self.provider\n        repository = self.provider.repository\n        target = self.target\n        python_req = PythonRequirement.from_pyspec_set(PySpecSet(target.requires_python))\n        requirements: list[Requirement] = [python_req, *self.requirements]\n        max_rounds = self.project.config[\"strategy.resolve_max_rounds\"]\n        result = resolver.resolve(requirements, max_rounds)\n\n        if repository.has_warnings:\n            self.project.core.ui.info(\n                \"Use `-q/--quiet` to suppress these warnings, or ignore them per-package with \"\n                r\"`ignore_package_warnings` config in \\[tool.pdm] table.\",\n                verbosity=termui.Verbosity.NORMAL,\n            )\n\n        mapping = cast(dict[str, Candidate], result.mapping)\n        mapping.pop(\"python\", None)\n\n        local_name = normalize_name(self.project.name) if self.project.is_distribution else None\n        for key, candidate in list(mapping.items()):\n            if key is None:\n                continue\n            # For source distribution whose name can only be determined after it is built,\n            # the key in the resolution map and criteria should be updated.\n            if key.startswith(\":empty:\"):\n                new_key = provider.identify(candidate)\n                mapping[new_key] = mapping.pop(key)\n                result.criteria[new_key] = result.criteria.pop(key)  # type: ignore[attr-defined]\n\n        if FLAG_INHERIT_METADATA in self.strategies:\n            all_markers = merge_markers(result)\n            populate_groups(result)\n        else:\n            all_markers = {}\n\n        for key, candidate in list(mapping.items()):\n            if key in all_markers:\n                marker = all_markers[key]\n                if marker.is_empty():\n                    del mapping[key]\n                    continue\n                candidate.req = replace(candidate.req, marker=None if marker.is_any() else marker)\n\n            if not self.keep_self and strip_extras(key)[0] == local_name:\n                del mapping[key]\n\n        return mapping\n"
  },
  {
    "path": "src/pdm/resolver/uv.py",
    "content": "from __future__ import annotations\n\nimport logging\nimport os\nimport re\nimport subprocess\nfrom dataclasses import dataclass, replace\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any\n\nfrom pdm._types import HiddenText\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.markers import get_marker\nfrom pdm.models.repositories import Package\nfrom pdm.models.requirements import FileRequirement, NamedRequirement, Requirement, VcsRequirement\nfrom pdm.models.specifiers import get_specifier\nfrom pdm.project.lockfile import FLAG_DIRECT_MINIMAL_VERSIONS, FLAG_INHERIT_METADATA\nfrom pdm.resolver.base import Resolution, Resolver\nfrom pdm.resolver.reporters import RichLockReporter\nfrom pdm.termui import Verbosity\nfrom pdm.utils import normalize_name\n\nif TYPE_CHECKING:\n    from pdm._types import FileHash\n\nlogger = logging.getLogger(__name__)\n\nGIT_URL = re.compile(r\"(?P<repo>[^:/]+://[^\\?#]+)(?:\\?rev=(?P<ref>[^#]+?))?(?:#(?P<revision>[a-f0-9]+))$\")\n\n\n@dataclass\nclass UvResolver(Resolver):\n    def __post_init__(self) -> None:\n        super().__post_init__()\n\n        if self.locked_repository is None:\n            self.locked_repository = self.project.get_locked_repository()\n        if self.update_strategy not in {\"reuse\", \"all\"}:\n            self.project.core.ui.warn(\n                f\"{self.update_strategy} update strategy is not supported by uv, using 'reuse' instead\"\n            )\n            self.update_strategy = \"reuse\"\n        if FLAG_INHERIT_METADATA in self.strategies:\n            self.project.core.ui.warn(\"inherit_metadata strategy is not supported by uv resolver, it will be ignored\")\n            self.strategies.discard(FLAG_INHERIT_METADATA)\n        this_spec = self.environment.spec\n        assert this_spec.platform is not None\n        if self.target.platform and (\n            self.target.platform.sys_platform != this_spec.platform.sys_platform\n            or self.target.platform.arch != this_spec.platform.arch\n        ):\n            self.project.core.ui.warn(\n                f\"Resolving against target {self.target.platform} on {this_spec.platform} is not supported by uv mode, \"\n                \"the resolution may be inaccurate.\"\n            )\n\n    def _build_lock_command(self) -> list[str | HiddenText]:\n        cmd: list[str | HiddenText] = [\n            *self.project.core.uv_cmd,\n            \"lock\",\n            \"-p\",\n            str(self.environment.interpreter.executable),\n        ]\n        if self.project.core.ui.verbosity > 0:\n            cmd.append(\"--verbose\")\n        if not self.project.core.state.enable_cache:\n            cmd.append(\"--no-cache\")\n        first_index = True\n        for source in self.project.sources:\n            url = source.url_with_credentials\n            if source.type == \"find_links\":\n                cmd.extend([\"--find-links\", url])\n            elif first_index:\n                cmd.extend([\"--index-url\", url])\n                first_index = False\n            else:\n                cmd.extend([\"--extra-index-url\", url])\n        if self.project.pyproject.settings.get(\"resolution\", {}).get(\"respect-source-order\", False):\n            cmd.append(\"--index-strategy=unsafe-first-match\")\n        else:\n            cmd.append(\"--index-strategy=unsafe-best-match\")\n        if self.update_strategy != \"all\":\n            for name in self.tracked_names:\n                cmd.extend([\"-P\", name])\n        if self.project.pyproject.allow_prereleases:\n            cmd.append(\"--prerelease=allow\")\n        no_binary = self.environment._setting_list(\"PDM_NO_BINARY\", \"resolution.no-binary\")\n        only_binary = self.environment._setting_list(\"PDM_ONLY_BINARY\", \"resolution.only-binary\")\n        if \":all:\" in no_binary:\n            cmd.append(\"--no-binary\")\n        else:\n            for pkg in no_binary:\n                cmd.extend([\"--no-binary-package\", pkg])\n        if \":all:\" in only_binary:\n            cmd.append(\"--no-build\")\n        else:\n            for pkg in only_binary:\n                cmd.extend([\"--no-build-package\", pkg])\n        if not self.project.core.state.build_isolation:\n            cmd.append(\"--no-build-isolation\")\n        if cs := self.project.core.state.config_settings:\n            for k, v in cs.items():\n                cmd.extend([\"--config-setting\", f\"{k}={v}\"])\n\n        if FLAG_DIRECT_MINIMAL_VERSIONS in self.strategies:\n            cmd.append(\"--resolution=lowest-direct\")\n\n        if dt := self.project.core.state.exclude_newer:\n            cmd.extend([\"--exclude-newer\", dt.isoformat()])\n\n        return cmd\n\n    def _parse_uv_lock(self, path: Path) -> Resolution:\n        from unearth import Link\n\n        from pdm.compat import tomllib\n\n        with path.open(\"rb\") as f:\n            data = tomllib.load(f)\n\n        packages: list[Package] = []\n        hash_cache = self.project.make_hash_cache()\n        session = self.environment.session\n\n        def make_requirement(dep: dict[str, Any]) -> str:\n            req = NamedRequirement(name=dep[\"name\"])\n            if version := dep.get(\"version\"):\n                req.specifier = get_specifier(f\"=={version}\")\n            if marker := dep.get(\"marker\"):\n                req.marker = get_marker(marker)\n            if extra := dep.get(\"extra\"):\n                req.extras = extra\n            return req.as_line()\n\n        def make_hash(item: dict[str, Any], fallback_url: str | None = None) -> FileHash:\n            url = item.get(\"url\") or fallback_url\n            if url is None:\n                raise KeyError(\"url\")\n            link = Link(url)\n            hash_value = item.get(\"hash\")\n            if hash_value is None:\n                hash_value = hash_cache.get_hash(link, session)\n            return {\"url\": url, \"file\": link.filename, \"hash\": hash_value}\n\n        for package in data[\"package\"]:\n            if (\n                self.project.name\n                and package[\"name\"] == normalize_name(self.project.name)\n                and (not self.keep_self or package[\"source\"].get(\"virtual\"))\n            ):\n                continue\n            req: Requirement\n            if url := package[\"source\"].get(\"url\"):\n                req = FileRequirement.create(url=url, name=package[\"name\"])\n            elif git := package[\"source\"].get(\"git\"):\n                matches = GIT_URL.match(git)\n                if not matches:\n                    raise ValueError(f\"Invalid git URL: {git}\")\n                url = f\"git+{matches.group('repo')}\"\n                if ref := matches.group(\"ref\"):\n                    url += f\"@{ref}\"\n                req = VcsRequirement.create(url=url, name=package[\"name\"])\n                req.revision = matches.group(\"revision\")\n            elif editable := package[\"source\"].get(\"editable\"):\n                req = FileRequirement.create(path=editable, name=package[\"name\"], editable=True)\n            elif filepath := package[\"source\"].get(\"path\"):\n                req = FileRequirement.create(path=filepath, name=package[\"name\"])\n            else:\n                req = NamedRequirement.create(name=package[\"name\"], specifier=f\"=={package['version']}\")\n            candidate = Candidate(req, name=package[\"name\"], version=package[\"version\"])\n\n            fallback_url = package[\"source\"].get(\"url\")\n            for wheel in package.get(\"wheels\", []):\n                candidate.hashes.append(make_hash(wheel, fallback_url))\n            if sdist := package.get(\"sdist\"):\n                candidate.hashes.append(make_hash(sdist, fallback_url))\n            entry = Package(candidate, [make_requirement(dep) for dep in package.get(\"dependencies\", [])], \"\")\n            packages.append(entry)\n            if optional_dependencies := package.get(\"optional-dependencies\"):\n                for group, deps in optional_dependencies.items():\n                    extra_entry = Package(\n                        candidate.copy_with(replace(req, extras=(group,))),\n                        [f\"{req.key}=={candidate.version}\", *(make_requirement(dep) for dep in deps)],\n                        \"\",\n                    )\n                    packages.append(extra_entry)\n        return Resolution(packages, self.requested_groups)\n\n    def resolve(self) -> Resolution:\n        from pdm.formats.uv import uv_file_builder\n\n        locked_repo = self.locked_repository or self.project.get_locked_repository()\n        with uv_file_builder(self.project, str(self.target.requires_python), self.requirements, locked_repo) as builder:\n            venv_project = self.environment.interpreter.get_venv()\n            if venv_project is not None:\n                python_home = venv_project.root\n            else:  # pragma: no cover\n                python_home = self.environment.interpreter.path.parent\n                if python_home.name in (\"bin\", \"Scripts\"):\n                    python_home = python_home.parent\n            builder.build_pyproject_toml()\n            uv_lock_path = self.project.root / \"uv.lock\"\n            if self.update_strategy != \"all\":\n                builder.build_uv_lock()\n            try:\n                if isinstance(self.reporter, RichLockReporter):\n                    self.reporter.stop()\n                uv_lock_command = self._build_lock_command()\n                self.project.core.ui.echo(f\"Running uv lock command: {uv_lock_command}\", verbosity=Verbosity.DETAIL)\n                real_command = [s.secret if isinstance(s, HiddenText) else s for s in uv_lock_command]\n                env = {**os.environ, \"UV_PROJECT_ENVIRONMENT\": str(python_home)}\n                subprocess.run(real_command, cwd=self.project.root, check=True, env=env)\n            finally:\n                if isinstance(self.reporter, RichLockReporter):\n                    self.reporter.start()\n            return self._parse_uv_lock(uv_lock_path)\n"
  },
  {
    "path": "src/pdm/signals.py",
    "content": "\"\"\"\nThe signal definition for PDM.\n\nExample:\n    ```python\n    from pdm.signals import post_init, post_install\n\n    def on_post_init(project):\n        project.core.ui.echo(\"Project initialized\")\n    # Connect to the signal\n    post_init.connect(on_post_init)\n    # Or use as a decorator\n    @post_install.connect\n    def on_post_install(project, candidates, dry_run):\n        project.core.ui.echo(\"Project install succeeded\")\n    ```\n\"\"\"\n\nfrom blinker import NamedSignal, Namespace\n\npdm_signals = Namespace()\n\npost_init: NamedSignal = pdm_signals.signal(\"post_init\")\n\"\"\"Called after a project is initialized.\nArgs:\n    project (Project): The project object\n\"\"\"\npre_lock: NamedSignal = pdm_signals.signal(\"pre_lock\")\n\"\"\"Called before a project is locked.\nArgs:\n    project (Project): The project object\n    requirements (list[Requirement]): The requirements to lock\n    dry_run (bool): If true, won't perform any actions\n\"\"\"\npost_lock: NamedSignal = pdm_signals.signal(\"post_lock\")\n\"\"\"Called after a project is locked.\n\nArgs:\n    project (Project): The project object\n    resolution (dict[str, list[Candidate]]): The resolved candidates\n    dry_run (bool): If true, won't perform any actions\n\"\"\"\npre_install: NamedSignal = pdm_signals.signal(\"pre_install\")\n\"\"\"Called before a project is installed.\n\nArgs:\n    project (Project): The project object\n    packages (list[Package]): The packages to install\n    dry_run (bool): If true, won't perform any actions\n\"\"\"\npost_install: NamedSignal = pdm_signals.signal(\"post_install\")\n\"\"\"Called after a project is installed.\n\nArgs:\n    project (Project): The project object\n    packages (list[Package]): The packages installed\n    dry_run (bool): If true, won't perform any actions\n\"\"\"\npre_build: NamedSignal = pdm_signals.signal(\"pre_build\")\n\"\"\"Called before a project is built.\n\nArgs:\n    project (Project): The project object\n    dest (str): The destination location\n    config_settings (dict[str, str]|None): Additional config settings passed via args\n\"\"\"\npost_build: NamedSignal = pdm_signals.signal(\"post_build\")\n\"\"\"Called after a project is built.\n\nArgs:\n    project (Project): The project object\n    artifacts (Sequence[str]): The locations of built artifacts\n    config_settings (dict[str, str]|None): Additional config settings passed via args\n\"\"\"\npre_publish: NamedSignal = pdm_signals.signal(\"pre_publish\")\n\"\"\"Called before a project is published.\n\nArgs:\n    project (Project): The project object\n\"\"\"\npost_publish: NamedSignal = pdm_signals.signal(\"post_publish\")\n\"\"\"Called after a project is published.\n\nArgs:\n    project (Project): The project object\n\"\"\"\npre_run: NamedSignal = pdm_signals.signal(\"pre_run\")\n\"\"\"Called before any run.\n\nArgs:\n    project (Project): The project object\n    script (str): the script name\n    args (Sequence[str]): the command line provided arguments\n\"\"\"\npost_run: NamedSignal = pdm_signals.signal(\"post_run\")\n\"\"\"Called after any run.\n\nArgs:\n    project (Project): The project object\n    script (str): the script name\n    args (Sequence[str]): the command line provided arguments\n\"\"\"\npre_script: NamedSignal = pdm_signals.signal(\"pre_script\")\n\"\"\"Called before any script.\n\nArgs:\n    project (Project): The project object\n    script (str): the script name\n    args (Sequence[str]): the command line provided arguments\n\"\"\"\npost_script: NamedSignal = pdm_signals.signal(\"post_script\")\n\"\"\"Called after any script.\n\nArgs:\n    project (Project): The project object\n    script (str): the script name\n    args (Sequence[str]): the command line provided arguments\n\"\"\"\npost_use: NamedSignal = pdm_signals.signal(\"post_use\")\n\"\"\"Called after use switched to a new Python version.\n\nArgs:\n    project (Project): The project object\n    python (PythonInfo): Information about the new Python interpreter\n\"\"\"\npre_invoke: NamedSignal = pdm_signals.signal(\"pre_invoke\")\n\"\"\"Called before any command is invoked.\n\nArgs:\n    project (Project): The project object\n    command (str | None): the command name\n    options (Namespace): the parsed arguments\n\"\"\"\n"
  },
  {
    "path": "src/pdm/termui.py",
    "content": "from __future__ import annotations\n\nimport contextlib\nimport enum\nimport logging\nimport os\nimport tempfile\nimport warnings\nfrom typing import IO, TYPE_CHECKING\n\nimport rich\nfrom rich.box import ROUNDED\nfrom rich.console import Console\nfrom rich.progress import Progress, ProgressColumn\nfrom rich.prompt import Confirm, IntPrompt, Prompt\nfrom rich.table import Table\nfrom rich.theme import Theme\n\nfrom pdm.exceptions import PDMWarning\n\nif TYPE_CHECKING:\n    from typing import Any, Iterator, Sequence\n\n    from pdm._types import RichProtocol, Spinner, SpinnerT\n\nlogger = logging.getLogger(__name__)\nlogger.addHandler(logging.NullHandler())\nlogger.propagate = False\nlogger.setLevel(logging.DEBUG)\nunearth_logger = logging.getLogger(\"unearth\")\nunearth_logger.addHandler(logging.NullHandler())\nunearth_logger.propagate = False\nunearth_logger.setLevel(logging.DEBUG)\n\nDEFAULT_THEME = {\n    \"primary\": \"cyan\",\n    \"success\": \"green\",\n    \"warning\": \"yellow\",\n    \"error\": \"red\",\n    \"info\": \"blue\",\n    \"req\": \"bold green\",\n}\nrich.reconfigure(highlight=False, theme=Theme(DEFAULT_THEME))\n_err_console = Console(stderr=True, theme=Theme(DEFAULT_THEME))\n\n\ndef is_interactive(console: Console | None = None) -> bool:\n    \"\"\"Check if the terminal is run under interactive mode\"\"\"\n    if console is None:\n        console = rich.get_console()\n    return \"PDM_NON_INTERACTIVE\" not in os.environ and console.is_interactive\n\n\ndef is_legacy_windows(console: Console | None = None) -> bool:\n    \"\"\"Legacy Windows renderer may have problem rendering emojis\"\"\"\n    if console is None:\n        console = rich.get_console()\n    return console.legacy_windows\n\n\ndef style(text: str, *args: str, style: str | None = None, **kwargs: Any) -> str:\n    \"\"\"return text with ansi codes using rich console\n\n    :param text: message with rich markup, defaults to \"\".\n    :param style: rich style to apply to whole string\n    :return: string containing ansi codes\n    \"\"\"\n    _console = rich.get_console()\n    if _console.legacy_windows or not _console.is_terminal:  # pragma: no cover\n        return text\n    with _console.capture() as capture:\n        _console.print(text, *args, end=\"\", style=style, **kwargs)\n    return capture.get()\n\n\ndef confirm(*args: str, **kwargs: Any) -> bool:\n    default = kwargs.setdefault(\"default\", False)\n    if not is_interactive():\n        return default\n    return Confirm.ask(*args, **kwargs)\n\n\ndef ask(*args: str, prompt_type: type[str] | type[int] | None = None, **kwargs: Any) -> str:\n    \"\"\"prompt user and return response\n\n    :prompt_type: which rich prompt to use, defaults to str.\n    :raises ValueError: unsupported prompt type\n    :return: str of user's selection\n    \"\"\"\n    if not prompt_type or prompt_type is str:\n        return Prompt.ask(*args, **kwargs)\n    elif prompt_type is int:\n        return str(IntPrompt.ask(*args, **kwargs))\n    else:\n        raise ValueError(f\"unsupported {prompt_type}\")\n\n\nclass Verbosity(enum.IntEnum):\n    QUIET = -1\n    NORMAL = 0\n    DETAIL = 1\n    DEBUG = 2\n\n\nLOG_LEVELS = {\n    Verbosity.NORMAL: logging.WARN,\n    Verbosity.DETAIL: logging.INFO,\n    Verbosity.DEBUG: logging.DEBUG,\n}\n\n\nclass Emoji:\n    if is_legacy_windows():\n        SUCC = \"v\"\n        FAIL = \"x\"\n        LOCK = \" \"\n        POPPER = \" \"\n        ELLIPSIS = \"...\"\n        ARROW_SEPARATOR = \">\"\n    else:\n        SUCC = \":heavy_check_mark:\"\n        FAIL = \":heavy_multiplication_x:\"\n        LOCK = \":lock:\"\n        POPPER = \":party_popper:\"\n        ELLIPSIS = \"…\"\n        ARROW_SEPARATOR = \"➤\"\n\n\nif is_legacy_windows():\n    SPINNER = \"line\"\nelse:\n    SPINNER = \"dots\"\n\n\nclass DummySpinner:\n    \"\"\"A dummy spinner class implementing needed interfaces.\n    But only display text onto screen.\n    \"\"\"\n\n    def __init__(self, text: str) -> None:\n        self.text = text\n\n    def _show(self) -> None:\n        _err_console.print(f\"[primary]STATUS:[/] {self.text}\")\n\n    def update(self, text: str) -> None:\n        self.text = text\n        self._show()\n\n    def __enter__(self: SpinnerT) -> SpinnerT:\n        self._show()  # type: ignore[attr-defined]\n        return self\n\n    def __exit__(self, *args: Any) -> None:\n        pass\n\n\nclass SilentSpinner(DummySpinner):\n    def _show(self) -> None:\n        pass\n\n\nclass TruncatedIO:\n    \"\"\"A wrapper for IO that truncates output after certain length.\"\"\"\n\n    def __init__(self, wrapped: IO[str], max_length: int = 100 * 1024 * 1024) -> None:\n        self.max_length = max_length\n        self._fp = wrapped\n        self._truncated = False\n\n    def write(self, s: str) -> int:\n        if self._truncated:\n            return 0\n        if self._fp.tell() >= self.max_length:\n            s = \"...[truncated]...\\n\"\n            self._truncated = True\n            _err_console.print(\"[warning]WARNING:[/] Log truncated due to excessive length.\")\n        return self._fp.write(s)\n\n    def __getattr__(self, name: str) -> Any:\n        return getattr(self._fp, name)\n\n\nclass UI:\n    \"\"\"Terminal UI object\"\"\"\n\n    MAX_LOG_SIZE = 100 * 1024 * 1024  # 100MB\n\n    def __init__(\n        self, verbosity: Verbosity = Verbosity.NORMAL, *, exit_stack: contextlib.ExitStack | None = None\n    ) -> None:\n        self.verbosity = verbosity\n        self.exit_stack = exit_stack or contextlib.ExitStack()\n        self.log_dir: str | None = None\n\n    def set_verbosity(self, verbosity: int) -> None:\n        self.verbosity = Verbosity(verbosity)\n        if self.verbosity == Verbosity.QUIET:\n            self.exit_stack.enter_context(warnings.catch_warnings())\n            warnings.simplefilter(\"ignore\", PDMWarning, append=True)\n            warnings.simplefilter(\"ignore\", FutureWarning, append=True)\n\n    def set_theme(self, theme: Theme) -> None:\n        \"\"\"set theme for rich console\n\n        :param theme: dict of theme\n        \"\"\"\n        rich.get_console().push_theme(theme)\n        _err_console.push_theme(theme)\n\n    def echo(\n        self,\n        message: str | RichProtocol = \"\",\n        err: bool = False,\n        verbosity: Verbosity = Verbosity.QUIET,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"print message using rich console\n\n        :param message: message with rich markup, defaults to \"\".\n        :param err: if true print to stderr, defaults to False.\n        :param verbosity: verbosity level, defaults to QUIET.\n        \"\"\"\n        if self.verbosity >= verbosity:\n            console = _err_console if err else rich.get_console()\n            if not console.is_interactive:\n                kwargs.setdefault(\"crop\", False)\n                kwargs.setdefault(\"overflow\", \"ignore\")\n            console.print(message, **kwargs)\n\n    def display_columns(self, rows: Sequence[Sequence[str]], header: list[str] | None = None) -> None:\n        \"\"\"Print rows in aligned columns.\n\n        :param rows: a rows of data to be displayed.\n        :param header: a list of header strings.\n        \"\"\"\n\n        if header:\n            table = Table(box=ROUNDED)\n            for title in header:\n                if title[0] == \"^\":\n                    title, justify = title[1:], \"center\"\n                elif title[0] == \">\":\n                    title, justify = title[1:], \"right\"\n                else:\n                    title, justify = title, \"left\"\n                table.add_column(title, justify=justify)\n        else:\n            table = Table.grid(padding=(0, 1))\n            for _ in rows[0]:\n                table.add_column()\n        for row in rows:\n            table.add_row(*row)\n\n        rich.print(table)\n\n    @contextlib.contextmanager\n    def logging(self, type_: str = \"install\") -> Iterator[logging.Logger]:\n        \"\"\"A context manager that opens a file for logging when verbosity is NORMAL or\n        print to the stdout otherwise.\n        \"\"\"\n        log_file: str | None = None\n        if self.verbosity >= Verbosity.DETAIL:\n            handler: logging.Handler = logging.StreamHandler()\n            handler.setLevel(LOG_LEVELS[self.verbosity])\n        else:\n            if self.log_dir and not os.path.exists(self.log_dir):\n                os.makedirs(self.log_dir, exist_ok=True)\n            self._clean_logs()\n            fp, log_file = tempfile.mkstemp(\".log\", f\"pdm-{type_}-\", self.log_dir)\n            handler = logging.StreamHandler(\n                TruncatedIO(self.exit_stack.enter_context(open(fp, \"a\", encoding=\"utf-8\")), self.MAX_LOG_SIZE)\n            )\n            handler.setLevel(logging.DEBUG)\n\n        handler.setFormatter(logging.Formatter(\"%(name)s: %(message)s\"))\n        logger.addHandler(handler)\n        unearth_logger.addHandler(handler)\n\n        def cleanup() -> None:\n            if not log_file:\n                return\n            with contextlib.suppress(OSError):\n                os.unlink(log_file)\n\n        try:\n            yield logger\n        except Exception:\n            if self.verbosity < Verbosity.DETAIL:\n                logger.exception(\"Error occurs\")\n                self.echo(\n                    f\"See [warning]{log_file}[/] for detailed debug log.\",\n                    style=\"error\",\n                    err=True,\n                )\n            raise\n        else:\n            self.exit_stack.callback(cleanup)\n        finally:\n            logger.removeHandler(handler)\n            unearth_logger.removeHandler(handler)\n            handler.close()\n\n    def open_spinner(self, title: str) -> Spinner:\n        \"\"\"Open a spinner as a context manager.\"\"\"\n        if self.verbosity >= Verbosity.DETAIL or not is_interactive():\n            return DummySpinner(title)\n        else:\n            return _err_console.status(title, spinner=SPINNER, spinner_style=\"primary\")\n\n    def make_progress(self, *columns: str | ProgressColumn, **kwargs: Any) -> Progress:\n        \"\"\"create a progress instance for indented spinners\"\"\"\n        return Progress(*columns, disable=self.verbosity >= Verbosity.DETAIL, **kwargs)\n\n    def info(self, message: str, verbosity: Verbosity = Verbosity.NORMAL) -> None:\n        \"\"\"Print a message to stdout.\"\"\"\n        self.echo(f\"[info]INFO:[/] [dim]{message}[/]\", err=True, verbosity=verbosity)\n\n    def deprecated(self, message: str, verbosity: Verbosity = Verbosity.NORMAL) -> None:\n        \"\"\"Print a message to stdout.\"\"\"\n        self.echo(f\"[warning]DEPRECATED:[/] [dim]{message}[/]\", err=True, verbosity=verbosity)\n\n    def warn(self, message: str, verbosity: Verbosity = Verbosity.NORMAL) -> None:\n        \"\"\"Print a message to stdout.\"\"\"\n        self.echo(f\"[warning]WARNING:[/] {message}\", err=True, verbosity=verbosity)\n\n    def error(self, message: str, verbosity: Verbosity = Verbosity.QUIET) -> None:\n        \"\"\"Print a message to stdout.\"\"\"\n        self.echo(f\"[error]ERROR:[/] {message}\", err=True, verbosity=verbosity)\n\n    def _clean_logs(self) -> None:\n        import time\n        from pathlib import Path\n\n        if self.log_dir is None:\n            return\n        for file in Path(self.log_dir).iterdir():\n            if not file.is_file():\n                continue\n            if file.stat().st_ctime < time.time() - 7 * 24 * 60 * 60:  # 7 days\n                file.unlink()\n"
  },
  {
    "path": "src/pdm/utils.py",
    "content": "\"\"\"\nUtility functions\n\"\"\"\n\nfrom __future__ import annotations\n\nimport atexit\nimport contextlib\nimport functools\nimport inspect\nimport json\nimport os\nimport re\nimport shutil\nimport subprocess\nimport sys\nimport sysconfig\nimport tempfile\nimport urllib.parse as parse\nimport warnings\nfrom datetime import datetime, timezone\nfrom os import name as os_name\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Mapping\n\nfrom packaging.specifiers import InvalidSpecifier, SpecifierSet\nfrom packaging.version import Version\n\nfrom pdm.compat import importlib_metadata\nfrom pdm.exceptions import PDMDeprecationWarning, PdmException\n\nif TYPE_CHECKING:\n    from re import Match\n    from typing import IO, Any, Iterator\n\n    from pbs_installer import PythonVersion\n\n    from pdm._types import FileHash, HiddenText, RepositoryConfig\n    from pdm.compat import Distribution\n\ntry:\n    _packaging_version = importlib_metadata.version(\"packaging\")\nexcept Exception:\n    from packaging import __version__ as _packaging_version\n\n\n@functools.lru_cache(maxsize=1024)\ndef parse_version(version: str) -> Version:\n    return Version(version)\n\n\nPACKAGING_22 = parse_version(_packaging_version) >= parse_version(\"22\")\n\n\ndef create_tracked_tempdir(suffix: str | None = None, prefix: str | None = None, dir: str | None = None) -> str:\n    name = tempfile.mkdtemp(suffix=suffix, prefix=prefix, dir=dir)\n    os.makedirs(name, mode=0o777, exist_ok=True)\n\n    def clean_up() -> None:\n        shutil.rmtree(name, ignore_errors=True)\n\n    atexit.register(clean_up)\n    return name\n\n\ndef get_trusted_hosts(sources: list[RepositoryConfig]) -> list[str]:\n    \"\"\"Parse the project sources and return the trusted hosts\"\"\"\n    trusted_hosts = []\n    for source in sources:\n        assert source.url\n        url = source.url\n        netloc = parse.urlparse(url).netloc\n        host = netloc.rsplit(\"@\", 1)[-1]\n        if host not in trusted_hosts and source.verify_ssl is False:\n            trusted_hosts.append(host)\n    return trusted_hosts\n\n\ndef url_without_fragments(url: str) -> str:\n    return parse.urlunparse(parse.urlparse(url)._replace(fragment=\"\"))\n\n\ndef join_list_with(items: list[Any], sep: Any) -> list[Any]:\n    new_items = []\n    for item in items:\n        new_items.extend([item, sep])\n    return new_items[:-1]\n\n\ndef find_project_root(cwd: str = \".\") -> str | None:\n    \"\"\"Recursively find a `pyproject.toml` at given path or current working directory.\"\"\"\n    path = Path(cwd).absolute()\n    if list(path.glob(\"pyproject.toml\")):\n        return path.as_posix()\n\n    if path == path.parent:\n        return None\n\n    return find_project_root(str(path.parent))\n\n\ndef convert_hashes(files: list[FileHash]) -> dict[str, list[str]]:\n    \"\"\"Convert Pipfile.lock hash lines into InstallRequirement option format.\n\n    The option format uses a str-list mapping. Keys are hash algorithms, and\n    the list contains all values of that algorithm.\n    \"\"\"\n    result: dict[str, list[str]] = {}\n    for f in files:\n        hash_value = f.get(\"hash\", \"\")\n        name, has_name, hash_value = hash_value.partition(\":\")\n        if not has_name:\n            name, hash_value = \"sha256\", name\n        result.setdefault(name, []).append(hash_value)\n    return result\n\n\ndef get_user_email_from_git() -> tuple[str, str]:\n    \"\"\"Get username and email from git config.\n    Return empty if not configured or git is not found.\n    \"\"\"\n    git = shutil.which(\"git\")\n    if not git:\n        return \"\", \"\"\n    try:\n        username = subprocess.check_output([git, \"config\", \"user.name\"], text=True, encoding=\"utf-8\").strip()\n    except subprocess.CalledProcessError:\n        username = \"\"\n    try:\n        email = subprocess.check_output([git, \"config\", \"user.email\"], text=True, encoding=\"utf-8\").strip()\n    except subprocess.CalledProcessError:\n        email = \"\"\n    return username, email\n\n\ndef add_ssh_scheme_to_git_uri(uri: str) -> str:\n    \"\"\"Cleans VCS uris from pip format\"\"\"\n    # Add scheme for parsing purposes, this is also what pip does\n    if \"://\" not in uri:\n        uri = \"ssh://\" + uri\n        parsed = parse.urlparse(uri)\n        if \":\" in parsed.netloc:\n            netloc, _, path_start = parsed.netloc.rpartition(\":\")\n            path = f\"/{path_start}{parsed.path}\"\n            uri = parse.urlunparse(parsed._replace(netloc=netloc, path=path))\n    return uri\n\n\n@contextlib.contextmanager\ndef atomic_open_for_write(filename: str | Path, *, mode: str = \"w\", encoding: str = \"utf-8\") -> Iterator[IO]:\n    dirname = os.path.dirname(filename)\n    if not os.path.exists(dirname):\n        os.makedirs(dirname)\n    fd, name = tempfile.mkstemp(prefix=\"atomic-write-\", dir=dirname)\n    fp = open(fd, mode, encoding=encoding if \"b\" not in mode else None)\n    try:\n        yield fp\n    except Exception:\n        fp.close()\n        raise\n    else:\n        fp.close()\n        with contextlib.suppress(OSError):\n            os.unlink(filename)\n        # The tempfile is created with mode 600, we need to restore the default mode\n        # with copyfile() instead of move().\n        # See: https://github.com/pdm-project/pdm/issues/542\n        shutil.copyfile(name, str(filename))\n    finally:\n        os.unlink(name)\n\n\n@contextlib.contextmanager\ndef cd(path: str | Path) -> Iterator:\n    _old_cwd = os.getcwd()\n    os.chdir(path)\n    try:\n        yield\n    finally:\n        os.chdir(_old_cwd)\n\n\ndef url_to_path(url: str) -> str:\n    \"\"\"\n    Convert a file: URL to a path.\n    \"\"\"\n    from urllib.request import url2pathname\n\n    WINDOWS = sys.platform == \"win32\"\n\n    if not url.startswith(\"file:\"):\n        raise ValueError(f\"You can only turn file: urls into filenames (not {url!r})\")\n\n    _, netloc, path, _, _ = parse.urlsplit(url)\n\n    if not netloc or netloc == \"localhost\":\n        # According to RFC 8089, same as empty authority.\n        netloc = \"\"\n    elif WINDOWS:\n        # If we have a UNC path, prepend UNC share notation.\n        netloc = \"\\\\\\\\\" + netloc\n    else:\n        raise ValueError(f\"non-local file URIs are not supported on this platform: {url!r}\")\n\n    path = url2pathname(netloc + path)\n\n    # On Windows, urlsplit parses the path as something like \"/C:/Users/foo\".\n    # This creates issues for path-related functions like io.open(), so we try\n    # to detect and strip the leading slash.\n    if (\n        WINDOWS\n        and not netloc  # Not UNC.\n        and len(path) >= 3\n        and path[0] == \"/\"  # Leading slash to strip.\n        and path[1].isalpha()  # Drive letter.\n        and path[2:4] in (\":\", \":/\")  # Colon + end of string, or colon + absolute path.\n    ):\n        path = path[1:]\n\n    return path\n\n\ndef split_path_fragments(path: Path) -> tuple[Path, str]:\n    \"\"\"Split a path into fragments\"\"\"\n    left, sep, right = path.as_posix().partition(\"#egg=\")\n    return Path(left), sep + right\n\n\ndef expand_env_vars(credential: str, quote: bool = False, env: Mapping[str, str] | None = None) -> str:\n    \"\"\"A safe implementation of env var substitution.\n    It only supports the following forms:\n\n        ${ENV_VAR}\n\n    Neither $ENV_VAR and %ENV_VAR is supported.\n    \"\"\"\n    if env is None:\n        env = os.environ\n\n    def replace_func(match: Match) -> str:\n        rv = env.get(match.group(1), \"\")\n        return parse.quote(rv, \"\") if quote else rv\n\n    return re.sub(r\"\\$\\{(.+?)\\}\", replace_func, credential)\n\n\ndef expand_env_vars_in_auth(url: str) -> str:\n    \"\"\"In-place expand the auth in url\"\"\"\n    scheme, netloc, path, params, query, fragment = parse.urlparse(url)\n    if \"@\" in netloc:\n        auth, rest = netloc.split(\"@\", 1)\n        auth = expand_env_vars(auth, True)\n        netloc = \"@\".join([auth, rest])\n    return parse.urlunparse((scheme, netloc, path, params, query, fragment))\n\n\n@functools.lru_cache\ndef path_replace(pattern: str, replace_with: str, dest: str) -> str:\n    \"\"\"Safely replace the pattern in a path with given string.\n\n    :param pattern: the pattern to match\n    :param replace_with: the string to replace with\n    :param dest: the path to replace\n    :return the replaced path\n    \"\"\"\n    sub_flags = re.IGNORECASE if os_name == \"nt\" else 0\n    return re.sub(\n        pattern.replace(\"\\\\\", \"/\"),\n        replace_with,\n        dest.replace(\"\\\\\", \"/\"),\n        flags=sub_flags,\n    )\n\n\ndef is_path_relative_to(path: str | Path, other: str | Path) -> bool:\n    try:\n        Path(path).relative_to(other)\n    except ValueError:\n        return False\n    return True\n\n\ndef get_venv_like_prefix(interpreter: str | Path) -> tuple[Path | None, bool]:\n    \"\"\"Check if the given interpreter path is from a virtualenv,\n    and return two values: the root path and whether it's a conda env.\n    \"\"\"\n    interpreter = Path(interpreter)\n    prefix = interpreter.parent\n    if prefix.joinpath(\"conda-meta\").exists():\n        return prefix, True\n\n    prefix = prefix.parent\n    if prefix.joinpath(\"pyvenv.cfg\").exists():\n        return prefix, False\n    if prefix.joinpath(\"conda-meta\").exists():\n        return prefix, True\n\n    virtual_env = os.getenv(\"VIRTUAL_ENV\")\n    if virtual_env and is_path_relative_to(interpreter, virtual_env):\n        return Path(virtual_env), False\n    virtual_env = os.getenv(\"CONDA_PREFIX\")\n    if virtual_env and is_path_relative_to(interpreter, virtual_env):\n        return Path(virtual_env), True\n    return None, False\n\n\ndef find_python_in_path(path: str | Path) -> Path | None:\n    \"\"\"Find a python interpreter from the given path, the input argument could be:\n\n    - A valid path to the interpreter\n    - A Python root directory that contains the interpreter\n    \"\"\"\n    pathlib_path = Path(path).absolute()\n    if pathlib_path.is_file():\n        return pathlib_path\n\n    if os.name == \"nt\":\n        for root_dir in (pathlib_path, pathlib_path / \"Scripts\"):\n            if root_dir.joinpath(\"python.exe\").exists():\n                return root_dir.joinpath(\"python.exe\")\n    else:\n        executable_pattern = re.compile(r\"python(?:\\d(?:\\.\\d+m?)?)?$\")\n\n        for python in pathlib_path.joinpath(\"bin\").glob(\"python*\"):\n            if executable_pattern.match(python.name):\n                return python\n\n    return None\n\n\ndef get_rev_from_url(url: str) -> str:\n    \"\"\"Get the rev part from the VCS URL.\"\"\"\n    path = parse.urlparse(url).path\n    if \"@\" in path:\n        _, rev = path.rsplit(\"@\", 1)\n        return rev\n    return \"\"\n\n\n@functools.lru_cache\ndef normalize_name(name: str, lowercase: bool = True) -> str:\n    name = re.sub(r\"[^A-Za-z0-9]+\", \"-\", name)\n    return name.lower() if lowercase else name\n\n\ndef comparable_version(version: str) -> Version:\n    \"\"\"Normalize a version to make it valid in a specifier.\"\"\"\n    parsed = parse_version(version or \"0.0.0\")\n    if parsed.local is not None:\n        # strip the local part to make\n        # comparable_version(\"1.2.3+local1\") == Version(\"1.2.3\")\n        if hasattr(parsed, \"__replace__\"):  # packaging >= 26\n            parsed = parsed.__replace__(local=None)\n        else:\n            # packaging < 26 does not have __replace__ method\n            # In this version, we need to manually update _version and recompute _key\n            # Note: In packaging >= 26, _key is a read-only property, but this else branch\n            # only executes on packaging < 26 where _key is a regular attribute that can be\n            # assigned. We use object.__setattr__() instead of direct assignment to satisfy\n            # type checkers that analyze based on the current packaging version.\n            from packaging.version import _cmpkey\n\n            parsed._version = parsed._version._replace(local=None)\n\n            object.__setattr__(\n                parsed,\n                \"_key\",\n                _cmpkey(\n                    parsed._version.epoch,\n                    parsed._version.release,\n                    parsed._version.pre,\n                    parsed._version.post,\n                    parsed._version.dev,\n                    parsed._version.local,\n                ),\n            )\n\n    return parsed\n\n\ndef is_egg_link(dist: Distribution) -> bool:\n    \"\"\"Check if the distribution is an egg-link install\"\"\"\n    return getattr(dist, \"link_file\", None) is not None\n\n\ndef is_editable(dist: Distribution) -> bool:\n    \"\"\"Check if the distribution is installed in editable mode\"\"\"\n    if is_egg_link(dist):\n        return True\n    direct_url = dist.read_text(\"direct_url.json\")\n    if not direct_url:\n        return False\n    direct_url_data = json.loads(direct_url)\n    return direct_url_data.get(\"dir_info\", {}).get(\"editable\", False)\n\n\ndef pdm_scheme(base: str) -> dict[str, str]:\n    \"\"\"Return a PEP 582 style install scheme\"\"\"\n    if \"pep582\" not in sysconfig.get_scheme_names():\n        bin_prefix = \"Scripts\" if os.name == \"nt\" else \"bin\"\n        sysconfig._INSTALL_SCHEMES[\"pep582\"] = {  # type: ignore[attr-defined]\n            \"stdlib\": \"{pep582_base}/lib\",\n            \"platstdlib\": \"{pep582_base}/lib\",\n            \"purelib\": \"{pep582_base}/lib\",\n            \"platlib\": \"{pep582_base}/lib\",\n            \"include\": \"{pep582_base}/include\",\n            \"scripts\": f\"{{pep582_base}}/{bin_prefix}\",\n            \"data\": \"{pep582_base}\",\n            \"prefix\": \"{pep582_base}\",\n            \"headers\": \"{pep582_base}/include\",\n        }\n    return sysconfig.get_paths(\"pep582\", vars={\"pep582_base\": base}, expand=True)\n\n\ndef is_url(url: str) -> bool:\n    \"\"\"Check if the given string is a URL\"\"\"\n    return bool(parse.urlparse(url).scheme)\n\n\n@functools.lru_cache\ndef fs_supports_link_method(method: str) -> bool:\n    if not hasattr(os, method):\n        return False\n    if sys.platform == \"win32\":\n        with tempfile.TemporaryDirectory(prefix=\"TmP\") as temp_dir:\n            with open(src := os.path.join(temp_dir, \"a\"), \"w\") as tmp_file:\n                tmp_file.write(\"foo\")\n            dest = f\"{src}-link\"\n            try:\n                getattr(os, method)(src, dest)\n                return True\n            except (OSError, NotImplementedError):\n                return False\n    else:\n        return True\n\n\ndef deprecation_warning(message: str, stacklevel: int = 1, raise_since: str | None = None) -> None:  # pragma: no cover\n    \"\"\"Show a deprecation warning with the given message and raise an error\n    after a specified version.\n    \"\"\"\n    from pdm.__version__ import __version__\n\n    if raise_since is not None:\n        if parse_version(__version__) >= parse_version(raise_since):\n            raise PDMDeprecationWarning(message)\n    warnings.warn(message, PDMDeprecationWarning, stacklevel=stacklevel + 1)\n\n\ndef is_pip_compatible_with_python(python_version: Version | str) -> bool:\n    \"\"\"Check the given python version is compatible with the pip installed\"\"\"\n    from pdm.compat import importlib_metadata\n    from pdm.models.specifiers import get_specifier\n\n    pip = importlib_metadata.distribution(\"pip\")\n    requires_python = get_specifier(pip.metadata.get(\"Requires-Python\"))\n    return requires_python.contains(python_version, True)\n\n\ndef is_in_zipapp() -> bool:\n    \"\"\"Check if the current process is running in a zipapp\"\"\"\n    return not os.path.exists(__file__)\n\n\n@functools.lru_cache(None)\ndef package_installed(package_name: str) -> bool:\n    try:\n        importlib_metadata.distribution(package_name)\n    except importlib_metadata.PackageNotFoundError:\n        return False\n    else:\n        return True\n\n\ndef validate_project_name(name: str) -> bool:\n    \"\"\"Check if the project name is valid or not\"\"\"\n\n    pattern = r\"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$\"\n    return re.fullmatch(pattern, name, flags=re.IGNORECASE) is not None\n\n\ndef sanitize_project_name(name: str) -> str:\n    \"\"\"Sanitize the project name and remove all illegal characters\"\"\"\n    pattern = r\"[^a-zA-Z0-9\\-_\\.]+\"\n    result = re.sub(pattern, \"-\", name)\n    result = re.sub(r\"^[\\._-]|[\\._-]$\", \"\", result)\n    if not result:\n        raise PdmException(f\"Invalid project name: {name}\")\n    return result\n\n\ndef is_conda_base() -> bool:\n    return os.getenv(\"CONDA_DEFAULT_ENV\", \"\") == \"base\"\n\n\ndef is_conda_base_python(python: Path) -> bool:\n    if not is_conda_base():\n        return False\n    prefix = os.environ[\"CONDA_PREFIX\"]\n    try:\n        python.relative_to(prefix)\n    except ValueError:\n        return False\n    return True\n\n\ndef filtered_sources(sources: list[RepositoryConfig], package: str | None) -> list[RepositoryConfig]:\n    \"\"\"Get matching sources based on the index attribute.\"\"\"\n    source_preferences = [(s, _source_preference(package, s)) for s in sources]\n    included_by = [s for s, p in source_preferences if p is True]\n    if included_by:\n        return included_by\n    return [s for s, p in source_preferences if p is None]\n\n\ndef _source_preference(package: str | None, source: RepositoryConfig) -> bool | None:\n    import fnmatch\n\n    if package is None:\n        return None\n    key = normalize_name(package)\n    if any(fnmatch.fnmatch(key, pat) for pat in source.include_packages):\n        return True\n    if any(fnmatch.fnmatch(key, pat) for pat in source.exclude_packages):\n        return False\n    return None\n\n\ndef get_file_hash(filename: str | Path, algorithm: str = \"sha256\") -> str:\n    \"\"\"Calculate the hash of a file with the given algorithm\"\"\"\n    import hashlib\n\n    h = hashlib.new(algorithm)\n    with open(filename, \"rb\") as f:\n        for chunk in iter(lambda: f.read(8192), b\"\"):\n            h.update(chunk)\n    return h.hexdigest()\n\n\ndef convert_to_datetime(value: str) -> datetime:\n    if \"T\" in value:\n        return datetime.fromisoformat(value.replace(\"Z\", \"+00:00\"))\n    return datetime.strptime(value, \"%Y-%m-%d\").replace(tzinfo=timezone.utc)\n\n\ndef get_all_installable_python_versions(build_dir: bool = False) -> list[PythonVersion]:\n    \"\"\"Returns all installable standalone Python interpreter versions from @indygreg\n\n    Installable means:\n        Fitting current platform and arch\n\n    Parameters:\n        build_dir: Whether to include the `build/` directory from indygreg builds (aka 'Full Archive')\n    \"\"\"\n    from pbs_installer._install import THIS_ARCH, THIS_PLATFORM\n    from pbs_installer._versions import PYTHON_VERSIONS\n\n    arch = \"x86\" if THIS_ARCH == \"32\" else THIS_ARCH\n    matches = [v for v, u in PYTHON_VERSIONS.items() if any(k[:2] == (THIS_PLATFORM, arch) for k in u)]\n    return matches\n\n\ndef get_class_init_params(klass: type) -> set[str]:\n    arguments: set[str] = set()\n    for cls in klass.__mro__:\n        if \"__init__\" not in cls.__dict__:\n            continue\n        params = inspect.signature(cls).parameters\n        arguments.update({k for k, v in params.items() if v.kind not in (v.VAR_POSITIONAL, v.VAR_KEYWORD)})\n        if not any(p.kind in (p.VAR_POSITIONAL, p.VAR_KEYWORD) for p in params.values()):\n            break\n    return arguments\n\n\ndef get_requirement_from_override(name: str, value: str) -> str:\n    if is_url(value):\n        req = f\"{name} @ {value}\"\n    else:\n        try:\n            SpecifierSet(value)\n        except InvalidSpecifier:\n            req = f\"{name}=={value}\"\n        else:\n            req = f\"{name}{value}\"\n    return req\n\n\ndef hide_url(url: str) -> HiddenText:\n    \"\"\"Redact the URL for display purposes.\"\"\"\n    from pdm._types import HiddenText\n\n    parsed = parse.urlsplit(url)\n    if \"@\" not in parsed.netloc:\n        return HiddenText(url, url)\n    *_, netloc = parsed.netloc.rpartition(\"@\")\n    netloc = f\"*****@{netloc}\"\n    redacted = parse.urlunsplit((parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment))\n    return HiddenText(url, redacted)\n"
  },
  {
    "path": "tasks/complete.py",
    "content": "from pathlib import Path\n\nimport pycomplete\n\nfrom pdm.core import Core\n\nCOMPLETIONS = Path(__file__).parent.parent / \"src/pdm/cli/completions\"\n\n\ndef main():\n    core = Core()\n    core.init_parser()\n\n    completer = pycomplete.Completer(core.parser, [\"pdm\"])\n    for shell in (\"bash\", \"fish\"):\n        COMPLETIONS.joinpath(f\"pdm.{shell}\").write_text(completer.render(shell))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tasks/max_versions.py",
    "content": "from __future__ import annotations\n\nimport json\nfrom html.parser import HTMLParser\nfrom pathlib import Path\n\nimport httpx\n\nPROJECT_DIR = Path(__file__).parent.parent\n\n\nclass PythonVersionParser(HTMLParser):\n    def __init__(self, *, convert_charrefs: bool = True) -> None:\n        super().__init__(convert_charrefs=convert_charrefs)\n        self._parsing_release_number_span = False\n        self._parsing_release_number_a = False\n        self.parsed_python_versions: list[str] = []\n\n    def handle_starttag(self, tag: str, attrs: list[tuple[str, str]]) -> None:\n        if tag == \"span\" and any(\"release-number\" in value for key, value in attrs if key == \"class\"):\n            self._parsing_release_number_span = True\n            return\n\n        if self._parsing_release_number_span and tag == \"a\":\n            self._parsing_release_number_a = True\n\n    def handle_endtag(self, tag: str) -> None:\n        if self._parsing_release_number_span and tag == \"span\":\n            self._parsing_release_number_span = False\n\n        if self._parsing_release_number_a and tag == \"a\":\n            self._parsing_release_number_a = False\n\n    def handle_data(self, data: str) -> None:\n        if self._parsing_release_number_a:\n            self.parsed_python_versions.append(data[7:])\n\n\ndef dump_python_version_module(dest_file) -> None:\n    resp = httpx.get(\"https://python.org/downloads/\", follow_redirects=True)\n    resp_text = resp.text\n    parser = PythonVersionParser()\n    parser.feed(resp_text)\n    python_versions = sorted(parser.parsed_python_versions)\n    max_versions: dict[str, int] = {}\n    for version in python_versions:\n        major, minor, patch = version.split(\".\")\n        major_minor = f\"{major}.{minor}\"\n        if major not in max_versions or max_versions[major] < int(minor):\n            max_versions[major] = int(minor)\n        if major_minor not in max_versions or max_versions[major_minor] < int(patch):\n            max_versions[major_minor] = int(patch)\n    with open(dest_file, \"w\") as f:\n        json.dump(max_versions, f, sort_keys=True, indent=4)\n        f.write(\"\\n\")\n\n\nif __name__ == \"__main__\":\n    dump_python_version_module(PROJECT_DIR / \"src/pdm/models/python_max_versions.json\")\n"
  },
  {
    "path": "tasks/release.py",
    "content": "from __future__ import annotations\n\nimport argparse\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, cast\n\nimport parver\nfrom rich.console import Console\n\nif TYPE_CHECKING:\n    from parver._typing import PreTag\n\n_console = Console(highlight=False)\n_err_console = Console(stderr=True, highlight=False)\n\n\ndef echo(*args: str, err: bool = False, **kwargs: Any):\n    if err:\n        _err_console.print(*args, **kwargs)\n    else:\n        _console.print(*args, **kwargs)\n\n\nPROJECT_DIR = Path(__file__).parent.parent\n\n\ndef get_current_version() -> str:\n    return subprocess.check_output([\"git\", \"describe\", \"--abbrev=0\", \"--tags\"], cwd=PROJECT_DIR).decode().strip()\n\n\ndef bump_version(pre: str | None = None, major: bool = False, minor: bool = False) -> str:\n    if major and minor:\n        echo(\"Only one option should be provided among (--major, --minor)\", style=\"red\", err=True)\n        sys.exit(1)\n    current_version = parver.Version.parse(get_current_version())\n    if major or minor:\n        version_idx = [major, minor].index(True)\n        version = current_version.bump_release(index=version_idx)\n    elif pre is not None and current_version.is_prerelease:\n        version = current_version\n    else:\n        version = current_version.bump_release(index=2)\n    if pre is not None:\n        if version.pre_tag != pre:\n            version = version.replace(pre_tag=cast(\"PreTag\", pre), pre=0)\n        else:\n            version = version.bump_pre()\n    else:\n        version = version.replace(pre=None, post=None)\n    version = version.replace(local=None, dev=None)\n    return str(version)\n\n\ndef release(\n    dry_run: bool = False, commit: bool = True, pre: str | None = None, major: bool = False, minor: bool = False\n) -> None:\n    new_version = bump_version(pre, major, minor)\n    echo(f\"Bump version to: {new_version}\", style=\"yellow\")\n    if dry_run:\n        subprocess.check_call([\"towncrier\", \"build\", \"--version\", new_version, \"--draft\"])\n    else:\n        subprocess.check_call([\"towncrier\", \"build\", \"--yes\", \"--version\", new_version])\n        subprocess.check_call([\"git\", \"add\", \".\"])\n        if commit:\n            subprocess.check_call([\"git\", \"commit\", \"-m\", f\"chore: Release {new_version}\"])\n            subprocess.check_call([\"git\", \"tag\", \"-a\", new_version, \"-m\", f\"v{new_version}\"])\n            subprocess.check_call([\"git\", \"push\"])\n            subprocess.check_call([\"git\", \"push\", \"--tags\"])\n\n\ndef parse_args(argv=None):\n    parser = argparse.ArgumentParser(\"release.py\")\n\n    parser.add_argument(\"--dry-run\", action=\"store_true\", help=\"Dry run mode\")\n    parser.add_argument(\n        \"--no-commit\",\n        action=\"store_false\",\n        dest=\"commit\",\n        default=True,\n        help=\"Do not commit to Git\",\n    )\n    group = parser.add_argument_group(title=\"version part\")\n    group.add_argument(\"--pre\", help=\"Bump with the pre tag\", choices=[\"a\", \"b\", \"rc\"])\n    group.add_argument(\"--major\", action=\"store_true\", help=\"Bump major version\")\n    group.add_argument(\"--minor\", action=\"store_true\", help=\"Bump minor version\")\n\n    return parser.parse_args(argv)\n\n\nif __name__ == \"__main__\":\n    args = parse_args()\n    release(args.dry_run, args.commit, args.pre, args.major, args.minor)\n"
  },
  {
    "path": "tests/__init__.py",
    "content": "from pathlib import Path\n\nFIXTURES = Path(__file__).parent / \"fixtures\"\n"
  },
  {
    "path": "tests/cli/__init__.py",
    "content": ""
  },
  {
    "path": "tests/cli/conftest.py",
    "content": "from __future__ import annotations\n\nimport shutil\nimport textwrap\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Any\nfrom unittest.mock import MagicMock\n\nimport httpx\nimport pytest\nfrom pytest_mock import MockerFixture\n\nfrom pdm.cli.commands.publish.package import PackageFile\nfrom pdm.cli.commands.publish.repository import Repository\nfrom pdm.models.auth import Keyring, keyring\nfrom tests import FIXTURES\n\n\n@pytest.fixture\ndef mock_run_gpg(mocker: MockerFixture):\n    def mock_run_gpg(args):\n        signature_file = args[-1] + \".asc\"\n        with open(signature_file, \"wb\") as f:\n            f.write(b\"fake signature\")\n\n    mocker.patch.object(PackageFile, \"_run_gpg\", side_effect=mock_run_gpg)\n\n\n@pytest.fixture\ndef prepare_packages(tmp_path: Path):\n    dist_path = tmp_path / \"dist\"\n    dist_path.mkdir()\n    for filename in [\n        \"demo-0.0.1-py2.py3-none-any.whl\",\n        \"demo-0.0.1.tar.gz\",\n        \"demo-0.0.1.zip\",\n    ]:\n        shutil.copy2(FIXTURES / \"artifacts\" / filename, dist_path)\n\n\n@pytest.fixture\ndef mock_pypi(mocker: MockerFixture):\n    def send(request, **kwargs):\n        # consume the data body to make the progress complete\n        request.read()\n        return httpx.Response(status_code=200, request=request)\n\n    return mocker.patch(\"pdm.models.session.PDMPyPIClient.send\", side_effect=send)\n\n\n@pytest.fixture\ndef uploaded(mocker: MockerFixture):\n    packages = []\n\n    def fake_upload(package):\n        packages.append(package)\n        return httpx.Response(status_code=200, request=httpx.Request(\"POST\", \"https://upload.pypi.org/legacy/\"))\n\n    mocker.patch.object(Repository, \"upload\", side_effect=fake_upload)\n    return packages\n\n\n@dataclass\nclass PublishMock:\n    mock_pypi: MagicMock\n    uploaded: list[Any]\n\n\n@pytest.fixture\n# @pytest.mark.usefixtures(\"mock_run_gpg\", \"prepare_packages\")\ndef mock_publish(mock_pypi, uploaded) -> PublishMock:\n    return PublishMock(\n        mock_pypi=mock_pypi,\n        uploaded=uploaded,\n    )\n\n\n@pytest.fixture\ndef _echo(project):\n    \"\"\"\n    Provides an echo.py script producing cross-platform expectable outputs\n    \"\"\"\n    (project.root / \"echo.py\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            import os, sys, io\n            sys.stdout = io.TextIOWrapper(sys.stdout.buffer, newline='\\\\n')\n            name = sys.argv[1]\n            vars = \" \".join([f\"{v}={os.getenv(v)}\" for v in sys.argv[2:]])\n            print(f\"{name} CALLED with {vars}\" if vars else f\"{name} CALLED\")\n            \"\"\"\n        )\n    )\n\n\n@pytest.fixture(name=\"keyring\")\ndef keyring_fixture(mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch) -> Keyring:\n    from unearth.auth import AuthInfo, KeyringBaseProvider\n\n    class MockKeyringProvider(KeyringBaseProvider):\n        def __init__(self) -> None:\n            self._store: dict[str, dict[str, str]] = {}\n\n        def save_auth_info(self, url: str, username: str, password: str) -> None:\n            self._store.setdefault(url, {})[username] = password\n\n        def get_auth_info(self, url: str, username: str | None) -> AuthInfo | None:\n            d = self._store.get(url, {})\n            if username is not None and username in d:\n                return username, d[username]\n            if username is None and d:\n                return next(iter(d.items()))\n            return None\n\n        def delete_auth_info(self, url: str, username: str) -> None:\n            self._store.get(url, {}).pop(username, None)\n\n    provider = MockKeyringProvider()\n    mocker.patch(\"unearth.auth.get_keyring_provider\", return_value=provider)\n    monkeypatch.setattr(keyring, \"provider\", provider)\n    monkeypatch.setattr(keyring, \"enabled\", True)\n    keyring.get_auth_info.cache_clear()\n    return keyring\n"
  },
  {
    "path": "tests/cli/test_add.py",
    "content": "import shutil\n\nimport pytest\nfrom unearth import Link\n\nfrom pdm.models.markers import EnvSpec\nfrom pdm.models.specifiers import PySpecSet\nfrom pdm.pytest import Distribution\nfrom tests import FIXTURES\n\n\ndef test_add_package(project, working_set, dev_option, pdm):\n    pdm([\"add\", *dev_option, \"requests\"], obj=project, strict=True)\n    group = project.pyproject.dependency_groups[\"dev\"] if dev_option else project.pyproject.metadata[\"dependencies\"]\n\n    assert group[0] == \"requests>=2.19.1\"\n    locked_candidates = project.get_locked_repository().candidates\n    assert locked_candidates[\"idna\"].version == \"2.7\"\n    for package in (\"requests\", \"idna\", \"chardet\", \"urllib3\", \"certifi\"):\n        assert package in working_set\n\n\ndef test_add_package_no_lock(project, working_set, dev_option, pdm):\n    pdm([\"add\", *dev_option, \"--frozen-lockfile\", \"-v\", \"requests\"], obj=project, strict=True)\n    group = project.pyproject.dependency_groups[\"dev\"] if dev_option else project.pyproject.metadata[\"dependencies\"]\n\n    assert group[0] == \"requests>=2.19.1\"\n    assert not project.lockfile.exists()\n    for package in (\"requests\", \"idna\", \"chardet\", \"urllib3\", \"certifi\"):\n        assert package in working_set\n\n\ndef test_add_command(project, pdm, mocker):\n    do_add = mocker.patch(\"pdm.cli.commands.add.Command.do_add\")\n    pdm([\"add\", \"requests\"], obj=project)\n    do_add.assert_called_once()\n\n\ndef test_add_package_to_custom_group(project, working_set, pdm):\n    pdm([\"add\", \"requests\", \"--group\", \"test\"], obj=project, strict=True)\n\n    assert \"requests\" in project.pyproject.metadata[\"optional-dependencies\"][\"test\"][0]\n    locked_candidates = project.get_locked_repository().candidates\n    assert locked_candidates[\"idna\"].version == \"2.7\"\n    for package in (\"requests\", \"idna\", \"chardet\", \"urllib3\", \"certifi\"):\n        assert package in working_set\n\n\ndef test_add_package_to_custom_dev_group(project, working_set, pdm):\n    pdm([\"add\", \"requests\", \"--group\", \"test\", \"--dev\"], obj=project, strict=True)\n\n    dependencies = project.pyproject.dependency_groups[\"test\"]\n    assert \"requests\" in dependencies[0]\n    locked_candidates = project.get_locked_repository().candidates\n    assert locked_candidates[\"idna\"].version == \"2.7\"\n    for package in (\"requests\", \"idna\", \"chardet\", \"urllib3\", \"certifi\"):\n        assert package in working_set\n\n\n@pytest.mark.usefixtures(\"vcs\")\ndef test_add_editable_package(project, working_set, pdm):\n    # Ensure that correct python version is used.\n    project.environment.python_requires = PySpecSet(\">=3.6\")\n    pdm([\"add\", \"--dev\", \"demo\"], obj=project, strict=True)\n    pdm([\"add\", \"-de\", \"git+https://github.com/test-root/demo.git#egg=demo\"], obj=project, strict=True)\n\n    group = project.pyproject.dev_dependencies[\"dev\"]\n    assert group == [\"-e git+https://github.com/test-root/demo.git#egg=demo\"]\n    assert not project.pyproject.dependency_groups\n    locked_candidates = project.get_locked_repository().candidates\n    assert locked_candidates[\"demo\"].prepare(project.environment).revision == \"1234567890abcdef\"\n    assert working_set[\"demo\"].link_file\n    assert locked_candidates[\"idna\"].version == \"2.7\"\n    assert \"idna\" in working_set\n\n    pdm([\"sync\", \"--no-editable\"], obj=project, strict=True)\n    assert not working_set[\"demo\"].link_file\n\n\n@pytest.mark.usefixtures(\"vcs\", \"working_set\")\ndef test_add_editable_package_to_metadata_forbidden(project, pdm):\n    project.environment.python_requires = PySpecSet(\">=3.6\")\n    result = pdm([\"add\", \"-v\", \"-e\", \"git+https://github.com/test-root/demo.git#egg=demo\"], obj=project)\n    assert \"PdmUsageError\" in result.stderr\n    result = pdm([\"add\", \"-v\", \"-Gtest\", \"-e\", \"git+https://github.com/test-root/demo.git#egg=demo\"], obj=project)\n    assert \"PdmUsageError\" in result.stderr\n\n\n@pytest.mark.usefixtures(\"working_set\", \"vcs\")\ndef test_non_editable_override_editable(project, pdm):\n    project.environment.python_requires = PySpecSet(\">=3.6\")\n    url = \"git+https://github.com/test-root/demo.git#egg=demo\"\n    pdm([\"add\", \"--dev\", \"-e\", url], obj=project, strict=True)\n    pdm([\"add\", \"--dev\", url], obj=project, strict=True)\n    assert not project.get_dependencies(\"dev\")[0].editable\n\n\n@pytest.mark.usefixtures(\"working_set\", \"vcs\")\ndef test_add_editable_normal_dev_dependency(project, pdm):\n    project.environment.python_requires = PySpecSet(\">=3.6\")\n    url = \"git+https://github.com/test-root/demo.git#egg=demo\"\n    pdm([\"add\", \"--dev\", \"-e\", url], obj=project, strict=True)\n    pdm([\"add\", \"-d\", \"urllib3\"], obj=project, strict=True)\n    pdm([\"add\", \"-d\", \"idna\"], obj=project, strict=True)\n    dev_group = project.pyproject.settings[\"dev-dependencies\"][\"dev\"]\n    pep735_group = project.pyproject.dependency_groups[\"dev\"]\n    assert dev_group == [\"-e git+https://github.com/test-root/demo.git#egg=demo\"]\n    assert pep735_group == [\"urllib3>=1.22\", \"idna>=2.7\"]\n\n\n@pytest.mark.usefixtures(\"working_set\", \"vcs\")\ndef test_add_dev_dependency_with_existing_editables_group(project, pdm):\n    project.environment.python_requires = PySpecSet(\">=3.6\")\n    url = \"git+https://github.com/test-root/demo.git#egg=demo\"\n    pdm([\"add\", \"-dG\", \"editables\", \"-e\", url], obj=project, strict=True)\n    pdm([\"add\", \"-d\", \"urllib3\"], obj=project, strict=True)\n    pdm([\"add\", \"-dG\", \"named\", \"idna\"], obj=project, strict=True)\n    assert \"editables\" in project.pyproject.settings[\"dev-dependencies\"]\n    assert \"dev\" in project.pyproject.dependency_groups\n    assert \"named\" in project.pyproject.dependency_groups\n    editables_group = project.pyproject.settings[\"dev-dependencies\"][\"editables\"]\n    pep735_group = project.pyproject.dependency_groups[\"dev\"]\n    pep735_named_group = project.pyproject.dependency_groups[\"named\"]\n    assert editables_group == [\"-e git+https://github.com/test-root/demo.git#egg=demo\"]\n    assert \"editables\" not in project.pyproject.dependency_groups\n    assert pep735_group == [\"urllib3>=1.22\"]\n    assert pep735_named_group == [\"idna>=2.7\"]\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_add_remote_package_url(project, dev_option, pdm):\n    project.environment.python_requires = PySpecSet(\">=3.6\")\n    url = \"http://fixtures.test/artifacts/demo-0.0.1-py2.py3-none-any.whl\"\n    pdm([\"add\", *dev_option, url], obj=project, strict=True)\n    group = project.pyproject.dependency_groups[\"dev\"] if dev_option else project.pyproject.metadata[\"dependencies\"]\n    assert group[0] == f\"demo @ {url}\"\n\n\ndef test_add_no_install(project, working_set, pdm):\n    pdm([\"add\", \"--no-sync\", \"requests\"], obj=project, strict=True)\n    for package in (\"requests\", \"idna\", \"chardet\", \"urllib3\", \"certifi\"):\n        assert package not in working_set\n\n\n@pytest.mark.usefixtures(\"repository\")\ndef test_add_package_save_exact(project, pdm):\n    pdm([\"add\", \"--save-exact\", \"--no-sync\", \"requests\"], obj=project, strict=True)\n    assert project.pyproject.metadata[\"dependencies\"][0] == \"requests==2.19.1\"\n\n\n@pytest.mark.usefixtures(\"repository\")\ndef test_add_package_save_wildcard(project, pdm):\n    pdm([\"add\", \"--save-wildcard\", \"--no-sync\", \"requests\"], obj=project, strict=True)\n    assert project.pyproject.metadata[\"dependencies\"][0] == \"requests\"\n\n\n@pytest.mark.usefixtures(\"repository\")\ndef test_add_package_save_minimum(project, pdm):\n    pdm([\"add\", \"--save-minimum\", \"--no-sync\", \"requests\"], obj=project, strict=True)\n    assert project.pyproject.metadata[\"dependencies\"][0] == \"requests>=2.19.1\"\n\n\ndef test_add_package_update_reuse(project, repository, pdm):\n    pdm([\"add\", \"--no-sync\", \"--save-wildcard\", \"requests\", \"pytz\"], obj=project, strict=True)\n\n    locked_candidates = project.get_locked_repository().candidates\n    assert locked_candidates[\"requests\"].version == \"2.19.1\"\n    assert locked_candidates[\"chardet\"].version == \"3.0.4\"\n    assert locked_candidates[\"pytz\"].version == \"2019.3\"\n\n    repository.add_candidate(\"pytz\", \"2019.6\")\n    repository.add_candidate(\"chardet\", \"3.0.5\")\n    repository.add_candidate(\"requests\", \"2.20.0\")\n    repository.add_dependencies(\n        \"requests\",\n        \"2.20.0\",\n        [\n            \"certifi>=2017.4.17\",\n            \"chardet<3.1.0,>=3.0.2\",\n            \"idna<2.8,>=2.5\",\n            \"urllib3<1.24,>=1.21.1\",\n        ],\n    )\n    pdm([\"add\", \"--no-sync\", \"--save-wildcard\", \"requests\"], obj=project, strict=True)\n    locked_candidates = project.get_locked_repository().candidates\n    assert locked_candidates[\"requests\"].version == \"2.20.0\"\n    assert locked_candidates[\"chardet\"].version == \"3.0.4\"\n    assert locked_candidates[\"pytz\"].version == \"2019.3\"\n\n\ndef test_add_package_update_eager(project, repository, pdm):\n    pdm([\"add\", \"--no-sync\", \"--save-wildcard\", \"requests\", \"pytz\"], obj=project, strict=True)\n\n    locked_candidates = project.get_locked_repository().candidates\n    assert locked_candidates[\"requests\"].version == \"2.19.1\"\n    assert locked_candidates[\"chardet\"].version == \"3.0.4\"\n    assert locked_candidates[\"pytz\"].version == \"2019.3\"\n\n    repository.add_candidate(\"pytz\", \"2019.6\")\n    repository.add_candidate(\"chardet\", \"3.0.5\")\n    repository.add_candidate(\"requests\", \"2.20.0\")\n    repository.add_dependencies(\n        \"requests\",\n        \"2.20.0\",\n        [\n            \"certifi>=2017.4.17\",\n            \"chardet<3.1.0,>=3.0.2\",\n            \"idna<2.8,>=2.5\",\n            \"urllib3<1.24,>=1.21.1\",\n        ],\n    )\n    pdm([\"add\", \"--no-sync\", \"--save-wildcard\", \"--update-eager\", \"requests\"], obj=project, strict=True)\n    locked_candidates = project.get_locked_repository().candidates\n    assert locked_candidates[\"requests\"].version == \"2.20.0\"\n    assert locked_candidates[\"chardet\"].version == \"3.0.5\"\n    assert locked_candidates[\"pytz\"].version == \"2019.3\"\n\n\ndef test_add_package_with_mismatch_marker(project, working_set, pdm):\n    env = project.environment\n    env.__dict__[\"spec\"] = EnvSpec.from_spec(\"==3.11\", \"macos\", \"cpython\")\n    pdm([\"add\", \"requests\", \"pytz; platform_system!='Darwin'\"], obj=project, strict=True)\n    assert \"pytz\" not in working_set\n\n\ndef test_add_dependency_from_multiple_parents(project, working_set, pdm):\n    env = project.environment\n    env.__dict__[\"spec\"] = EnvSpec.from_spec(\"==3.11\", \"macos\", \"cpython\")\n    pdm([\"add\", \"requests\", \"chardet; platform_system!='Darwin'\"], obj=project, strict=True)\n    assert \"chardet\" in working_set\n\n\ndef test_add_packages_without_self(project, working_set, pdm):\n    project.environment.python_requires = PySpecSet(\">=3.6\")\n    pdm([\"add\", \"--no-self\", \"requests\"], obj=project, strict=True)\n    assert project.name not in working_set\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_add_package_unconstrained_rewrite_specifier(project, pdm):\n    project.environment.python_requires = PySpecSet(\">=3.6\")\n    pdm([\"add\", \"--no-self\", \"django\"], obj=project, strict=True)\n    locked_candidates = project.get_locked_repository().candidates\n    assert locked_candidates[\"django\"].version == \"2.2.9\"\n    assert project.pyproject.metadata[\"dependencies\"][0] == \"django>=2.2.9\"\n\n    pdm([\"add\", \"--no-self\", \"--unconstrained\", \"django-toolbar\"], obj=project, strict=True)\n    locked_candidates = project.get_locked_repository().candidates\n    assert locked_candidates[\"django\"].version == \"1.11.8\"\n    assert project.pyproject.metadata[\"dependencies\"][0] == \"django>=1.11.8\"\n\n\n@pytest.mark.usefixtures(\"working_set\", \"vcs\")\ndef test_add_cached_vcs_requirement(project, mocker, pdm):\n    project.environment.python_requires = PySpecSet(\">=3.6\")\n    url = \"git+https://github.com/test-root/demo.git@1234567890abcdef#egg=demo\"\n    built_path = FIXTURES / \"artifacts/demo-0.0.1-py2.py3-none-any.whl\"\n    wheel_cache = project.make_wheel_cache()\n    cache_path = wheel_cache.get_path_for_link(Link(url), project.environment.spec)\n    if not cache_path.exists():\n        cache_path.mkdir(parents=True)\n    shutil.copy2(built_path, cache_path)\n    downloader = mocker.patch(\"unearth.finder.unpack_link\")\n    builder = mocker.patch(\"pdm.builders.WheelBuilder.build\")\n    pdm([\"add\", \"--no-self\", url], obj=project, strict=True)\n    lockfile_entry = next(p for p in project.lockfile[\"package\"] if p[\"name\"] == \"demo\")\n    assert lockfile_entry[\"revision\"] == \"1234567890abcdef\"\n    downloader.assert_not_called()\n    builder.assert_not_called()\n\n\n@pytest.mark.usefixtures(\"repository\")\ndef test_add_with_dry_run(project, pdm):\n    result = pdm([\"add\", \"--dry-run\", \"requests\"], obj=project, strict=True)\n    project.pyproject.reload()\n    assert not project.get_dependencies()\n    assert \"requests 2.19.1\" in result.stdout\n    assert \"urllib3 1.22\" in result.stdout\n\n\ndef test_add_with_prerelease(project, working_set, pdm):\n    pdm([\"add\", \"--prerelease\", \"--save-compatible\", \"urllib3\"], obj=project, strict=True)\n    assert working_set[\"urllib3\"].version == \"1.23b0\"\n    assert project.pyproject.metadata[\"dependencies\"][0] == \"urllib3<2,>=1.23b0\"\n\n\ndef test_add_editable_package_with_extras(project, working_set, pdm):\n    project.environment.python_requires = PySpecSet(\">=3.6\")\n    dep_path = FIXTURES.joinpath(\"projects/demo\")\n    pdm([\"add\", \"-dGdev\", \"-e\", f\"{dep_path.as_posix()}[security]\"], obj=project, strict=True)\n    assert f\"-e {dep_path.as_uri()}#egg=demo[security]\" in project.use_pyproject_dependencies(\"dev\", True)[0]\n    assert \"demo\" in working_set\n    assert \"requests\" in working_set\n    assert \"urllib3\" in working_set\n\n\n@pytest.mark.usefixtures(\"repository\")\ndef test_add_dependency_with_extras(project, pdm):\n    project.environment.python_requires = PySpecSet(\">=3.6\")\n    pdm([\"add\", \"requests[security]\", \"--no-sync\"], obj=project, strict=True)\n    locked_repo = project.get_locked_repository()\n    extra_package = next(p for p in locked_repo.packages.values() if p.candidate.identify() == \"requests[security]\")\n    assert extra_package.dependencies == [\"pyOpenSSL>=0.14\", \"requests==2.19.1\"]\n    # test adding new package won't add duplicate dependencies\n    pdm([\"add\", \"pytz\", \"--no-sync\"], obj=project, strict=True)\n    locked_repo = project.get_locked_repository()\n    extra_package = next(p for p in locked_repo.packages.values() if p.candidate.identify() == \"requests[security]\")\n    assert extra_package.dependencies == [\"pyOpenSSL>=0.14\", \"requests==2.19.1\"]\n\n\ndef test_add_package_with_local_version(project, repository, working_set, pdm):\n    repository.add_candidate(\"foo\", \"1.0a0+local\")\n    pdm([\"add\", \"-v\", \"foo\"], obj=project, strict=True)\n    assert working_set[\"foo\"].version == \"1.0a0+local\"\n    dependencies, _ = project.use_pyproject_dependencies(\"default\")\n    assert dependencies[0] == \"foo>=1.0a0\"\n\n\ndef test_add_group_to_lockfile(project, working_set, pdm):\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    assert project.lockfile.groups == [\"default\"]\n    pdm([\"add\", \"--group\", \"tz\", \"pytz\"], obj=project, strict=True)\n    assert project.lockfile.groups == [\"default\", \"tz\"]\n    assert \"pytz\" in working_set\n\n\ndef test_add_group_to_lockfile_without_package(project, working_set, pdm):\n    project.add_dependencies([\"requests\"])\n    project.add_dependencies([\"pytz\"], to_group=\"tz\")\n    pdm([\"install\"], obj=project, strict=True)\n    assert \"pytz\" not in working_set\n    assert project.lockfile.groups == [\"default\"]\n    pdm([\"add\", \"--group\", \"tz\"], obj=project, strict=True)\n    assert project.lockfile.groups == [\"default\", \"tz\"]\n    assert \"pytz\" in working_set\n\n\ndef test_add_update_reuse_installed(project, working_set, repository, pdm):\n    working_set[\"foo\"] = Distribution(\"foo\", \"1.0.0\")\n    repository.add_candidate(\"foo\", \"1.0.0\")\n    repository.add_candidate(\"foo\", \"1.1.0\")\n    pdm([\"add\", \"--update-reuse-installed\", \"foo\"], obj=project, strict=True)\n    locked_candidates = project.get_locked_repository().candidates\n    assert locked_candidates[\"foo\"].version == \"1.0.0\"\n\n\ndef test_add_update_reuse_installed_config(project, working_set, repository, pdm):\n    working_set[\"foo\"] = Distribution(\"foo\", \"1.0.0\")\n    repository.add_candidate(\"foo\", \"1.0.0\")\n    repository.add_candidate(\"foo\", \"1.1.0\")\n    project.project_config[\"strategy.update\"] = \"reuse-installed\"\n    pdm([\"add\", \"foo\"], obj=project, strict=True)\n    locked_candidates = project.get_locked_repository().candidates\n    assert locked_candidates[\"foo\"].version == \"1.0.0\"\n\n\ndef test_add_disable_cache(project, pdm, working_set):\n    cache_dir = project.cache_dir\n    pdm([\"--no-cache\", \"add\", \"requests\"], obj=project, strict=True)\n    assert \"requests\" in working_set\n\n    files = [file for file in cache_dir.rglob(\"*\") if file.is_file()]\n    assert not files\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_add_dependency_with_direct_minimal_versions(project, pdm, repository):\n    pdm([\"lock\", \"-S\", \"direct_minimal_versions\"], obj=project, strict=True)\n    repository.add_candidate(\"pytz\", \"2019.6\")\n    pdm([\"add\", \"django\"], obj=project, strict=True)\n    all_candidates = project.get_locked_repository().candidates\n    assert \"django>=1.11.8\" in project.pyproject.metadata[\"dependencies\"]\n    assert all_candidates[\"django\"].version == \"1.11.8\"\n    assert all_candidates[\"pytz\"].version == \"2019.6\"\n\n\ndef test_add_group_with_normalized_name(project, pdm, working_set):\n    project.pyproject.dependency_groups.update({\"foo_bar\": [\"requests\"]})\n    project.pyproject.write()\n    pdm([\"lock\"], obj=project, strict=True)\n    assert \"foo-bar\" in project.lockfile.groups\n    pdm([\"sync\", \"-G\", \"foo.bar\"], obj=project, strict=True)\n    assert \"requests\" in working_set\n    result = pdm([\"add\", \"-G\", \"foo-bar\", \"pytz\"], obj=project)\n    assert result.exit_code != 0\n    assert \"Group foo-bar already exists in another non-normalized form\" in result.stderr\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_add_to_dependency_group_with_include(project, pdm):\n    from pdm.formats.base import make_array\n\n    project.pyproject.dependency_groups.update({\"tz\": [\"pytz\"], \"web\": make_array([{\"include-group\": \"tz\"}])})\n    project.pyproject.write()\n    pdm([\"add\", \"-Gweb\", \"requests\"], obj=project, strict=True)\n    assert project.pyproject.dependency_groups[\"web\"] == [{\"include-group\": \"tz\"}, \"requests>=2.19.1\"]\n"
  },
  {
    "path": "tests/cli/test_build.py",
    "content": "import tarfile\nimport zipfile\nfrom unittest import mock\n\nimport pytest\n\nfrom pdm.cli.commands.build import Command\n\npytestmark = pytest.mark.usefixtures(\"local_finder\")\n\n\ndef get_tarball_names(path):\n    with tarfile.open(path, \"r:gz\") as tar:\n        return tar.getnames()\n\n\ndef get_wheel_names(path):\n    with zipfile.ZipFile(path) as zf:\n        return zf.namelist()\n\n\ndef test_build_command(project, pdm, mocker):\n    do_build = mocker.patch.object(Command, \"do_build\")\n    # prevent the context_settings from being reset\n    project.core.exit_stack.pop_all()\n    pdm([\"build\", \"--no-sdist\", \"-Ca=1\", \"--config-setting\", \"b=2\"], obj=project)\n    do_build.assert_called_with(\n        project,\n        sdist=False,\n        wheel=True,\n        dest=mock.ANY,\n        clean=True,\n        verbose=0,\n        hooks=mock.ANY,\n    )\n    assert project.core.state.config_settings == {\"a\": \"1\", \"b\": \"2\"}\n\n\ndef test_build_global_project_forbidden(pdm):\n    result = pdm([\"build\", \"-g\"])\n    assert result.exit_code != 0\n\n\ndef test_build_single_module(fixture_project):\n    project = fixture_project(\"demo-module\")\n\n    Command.do_build(project)\n    tar_names = get_tarball_names(project.root / \"dist/demo_module-0.1.0.tar.gz\")\n    for name in [\n        \"foo_module.py\",\n        \"bar_module.py\",\n        \"LICENSE\",\n        \"pyproject.toml\",\n        \"PKG-INFO\",\n    ]:\n        assert f\"demo_module-0.1.0/{name}\" in tar_names\n\n    for i in range(2):\n        if i == 1:\n            Command.do_build(project, sdist=False)\n        zip_names = get_wheel_names(project.root / \"dist/demo_module-0.1.0-py3-none-any.whl\")\n        for name in [\"foo_module.py\", \"bar_module.py\"]:\n            assert name in zip_names\n\n        for name in (\"pyproject.toml\", \"LICENSE\"):\n            assert name not in zip_names\n\n\ndef test_build_single_module_with_readme(fixture_project):\n    project = fixture_project(\"demo-module\")\n    project.pyproject.metadata[\"readme\"] = \"README.md\"\n    project.pyproject.write()\n    Command.do_build(project)\n    assert \"demo_module-0.1.0/README.md\" in get_tarball_names(project.root / \"dist/demo_module-0.1.0.tar.gz\")\n\n\ndef test_build_package(fixture_project):\n    project = fixture_project(\"demo-package\")\n    Command.do_build(project)\n\n    tar_names = get_tarball_names(project.root / \"dist/my_package-0.1.0.tar.gz\")\n    assert \"my_package-0.1.0/my_package/__init__.py\" in tar_names\n    assert \"my_package-0.1.0/my_package/data.json\" in tar_names\n    assert \"my_package-0.1.0/single_module.py\" not in tar_names\n    assert \"my_package-0.1.0/data_out.json\" not in tar_names\n\n    zip_names = get_wheel_names(project.root / \"dist/my_package-0.1.0-py3-none-any.whl\")\n    assert \"my_package/__init__.py\" in zip_names\n    assert \"my_package/data.json\" in zip_names\n    assert \"single_module.py\" not in zip_names\n    assert \"data_out.json\" not in zip_names\n\n\ndef test_build_src_package(fixture_project):\n    project = fixture_project(\"demo-src-package\")\n    Command.do_build(project)\n\n    tar_names = get_tarball_names(project.root / \"dist/demo_package-0.1.0.tar.gz\")\n    assert \"demo_package-0.1.0/src/my_package/__init__.py\" in tar_names\n    assert \"demo_package-0.1.0/src/my_package/data.json\" in tar_names\n\n    zip_names = get_wheel_names(project.root / \"dist/demo_package-0.1.0-py3-none-any.whl\")\n    assert \"my_package/__init__.py\" in zip_names\n    assert \"my_package/data.json\" in zip_names\n\n\ndef test_build_package_include(fixture_project):\n    project = fixture_project(\"demo-package\")\n    build_config = project.pyproject.settings.setdefault(\"build\", {})\n    build_config[\"includes\"] = [\n        \"my_package/\",\n        \"single_module.py\",\n        \"data_out.json\",\n    ]\n    build_config[\"excludes\"] = [\"my_package/*.json\"]\n    project.pyproject.write()\n    Command.do_build(project)\n\n    tar_names = get_tarball_names(project.root / \"dist/my_package-0.1.0.tar.gz\")\n    assert \"my_package-0.1.0/my_package/__init__.py\" in tar_names\n    assert \"my_package-0.1.0/my_package/data.json\" not in tar_names\n    assert \"my_package-0.1.0/single_module.py\" in tar_names\n    assert \"my_package-0.1.0/data_out.json\" in tar_names\n\n    zip_names = get_wheel_names(project.root / \"dist/my_package-0.1.0-py3-none-any.whl\")\n    assert \"my_package/__init__.py\" in zip_names\n    assert \"my_package/data.json\" not in zip_names\n    assert \"single_module.py\" in zip_names\n    assert \"data_out.json\" in zip_names\n\n\ndef test_build_src_package_by_include(fixture_project):\n    project = fixture_project(\"demo-src-package\")\n    project.pyproject.settings.setdefault(\"build\", {})[\"includes\"] = [\"src/my_package\"]\n    project.pyproject.write()\n    Command.do_build(project)\n\n    tar_names = get_tarball_names(project.root / \"dist/demo_package-0.1.0.tar.gz\")\n    assert \"demo_package-0.1.0/src/my_package/__init__.py\" in tar_names\n    assert \"demo_package-0.1.0/src/my_package/data.json\" in tar_names\n\n    zip_names = get_wheel_names(project.root / \"dist/demo_package-0.1.0-py3-none-any.whl\")\n    assert \"my_package/__init__.py\" in zip_names\n    assert \"my_package/data.json\" in zip_names\n\n\ndef test_build_with_config_settings(fixture_project):\n    project = fixture_project(\"demo-src-package\")\n    project.core.state.config_settings = {\"--plat-name\": \"win_amd64\"}\n    Command.do_build(project)\n\n    assert (project.root / \"dist/demo_package-0.1.0-py3-none-win_amd64.whl\").exists()\n\n\ndef test_cli_build_with_config_settings(fixture_project, pdm):\n    project = fixture_project(\"demo-src-package\")\n    result = pdm([\"build\", \"-C--plat-name=win_amd64\"], obj=project)\n    assert result.exit_code == 0\n    assert (project.root / \"dist/demo_package-0.1.0-py3-none-win_amd64.whl\").exists()\n\n\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_build_with_no_isolation(pdm, project):\n    result = pdm([\"build\", \"--no-isolation\"], obj=project)\n    assert result.exit_code == 1\n    pdm([\"add\", \"pdm-backend\", \"--no-self\"], obj=project, strict=True)\n    result = pdm([\"build\", \"--no-isolation\"], obj=project)\n    assert result.exit_code == 0\n\n\ndef test_build_ignoring_pip_environment(fixture_project, monkeypatch):\n    project = fixture_project(\"demo-module\")\n    monkeypatch.setenv(\"PIP_REQUIRE_VIRTUALENV\", \"1\")\n    Command.do_build(project)\n"
  },
  {
    "path": "tests/cli/test_cache.py",
    "content": "import pytest\nfrom unearth import Link\n\nfrom pdm.models.cached_package import CachedPackage\nfrom tests import FIXTURES\n\n\n@pytest.fixture\ndef prepare_wheel_cache(project):\n    cache_dir = project.cache(\"wheels\")\n    (cache_dir / \"arbitrary/path\").mkdir(parents=True)\n    for name in (\n        \"foo-0.1.0.whl\",\n        \"bar-0.2.0.whl\",\n        \"baz-0.3.0.whl\",\n        \"foo_bar-0.4.0.whl\",\n    ):\n        (cache_dir / \"arbitrary/path\" / name).touch()\n\n\n@pytest.fixture\ndef prepare_http_cache(project):\n    cache_dir = project.cache(\"http\")\n    (cache_dir / \"arbitrary/path\").mkdir(parents=True)\n    for name in (\n        \"foo-0.1.0.tar.gz\",\n        \"bar-0.2.0.zip\",\n        \"baz-0.3.0.tar.gz\",\n        \"foobar-0.4.0.tar.gz\",\n    ):\n        (cache_dir / \"arbitrary/path\" / name).touch()\n\n\n@pytest.mark.usefixtures(\"prepare_wheel_cache\")\ndef test_cache_list(project, pdm):\n    result = pdm([\"cache\", \"list\"], obj=project)\n    assert result.exit_code == 0\n\n    for name in (\n        \"foo-0.1.0.whl\",\n        \"bar-0.2.0.whl\",\n        \"baz-0.3.0.whl\",\n        \"foo_bar-0.4.0.whl\",\n    ):\n        assert name in result.output\n\n\n@pytest.mark.usefixtures(\"prepare_wheel_cache\")\ndef test_cache_list_pattern(project, pdm):\n    result = pdm([\"cache\", \"list\", \"ba*\"], obj=project)\n    assert result.exit_code == 0\n\n    for name in (\n        \"bar-0.2.0.whl\",\n        \"baz-0.3.0.whl\",\n    ):\n        assert name in result.output\n\n    for name in (\n        \"foo-0.1.0.whl\",\n        \"foo_bar-0.4.0.whl\",\n    ):\n        assert name not in result.output\n\n\n@pytest.mark.usefixtures(\"prepare_wheel_cache\", \"prepare_http_cache\")\ndef test_cache_remove_pattern(project, pdm):\n    result = pdm([\"cache\", \"remove\", \"ba*\"], obj=project)\n    assert result.exit_code == 0\n\n    for name in (\n        \"bar-0.2.0.whl\",\n        \"baz-0.3.0.whl\",\n    ):\n        assert not (project.cache(\"wheels\") / \"arbitrary/path\" / name).exists()\n\n    for name in (\n        \"foo-0.1.0.whl\",\n        \"foo_bar-0.4.0.whl\",\n    ):\n        assert (project.cache(\"wheels\") / \"arbitrary/path\" / name).exists()\n\n    assert (project.cache(\"http\") / \"arbitrary/path/foo-0.1.0.tar.gz\").exists()\n\n\n@pytest.mark.usefixtures(\"prepare_wheel_cache\", \"prepare_http_cache\")\ndef test_cache_remove_wildcard(project, pdm):\n    result = pdm([\"cache\", \"remove\", \"*\"], obj=project)\n    assert result.exit_code == 0\n\n    for name in (\n        \"bar-0.2.0.whl\",\n        \"baz-0.3.0.whl\",\n        \"foo-0.1.0.whl\",\n        \"foo_bar-0.4.0.whl\",\n    ):\n        assert not (project.cache(\"wheels\") / \"arbitrary/path\" / name).exists()\n\n    assert (project.cache(\"http\") / \"arbitrary/path/foo-0.1.0.tar.gz\").exists()\n\n\n@pytest.mark.usefixtures(\"prepare_wheel_cache\", \"prepare_http_cache\")\ndef test_cache_clear(project, pdm):\n    result = pdm([\"cache\", \"clear\"], obj=project)\n    assert result.exit_code == 0\n\n    for name in (\n        \"bar-0.2.0.whl\",\n        \"baz-0.3.0.whl\",\n        \"foo-0.1.0.whl\",\n        \"foo_bar-0.4.0.whl\",\n    ):\n        assert not (project.cache(\"wheels\") / \"arbitrary/path\" / name).exists()\n\n    assert not (project.cache(\"http\") / \"arbitrary/path/foo-0.1.0.tar.gz\").exists()\n\n\n@pytest.mark.usefixtures(\"prepare_wheel_cache\", \"prepare_http_cache\")\ndef test_cache_remove_no_pattern(project, pdm):\n    result = pdm([\"cache\", \"remove\"], obj=project)\n    assert result.exit_code != 0\n\n\n@pytest.mark.usefixtures(\"prepare_wheel_cache\", \"prepare_http_cache\")\ndef test_cache_info(project, pdm):\n    result = pdm([\"cache\", \"info\"], obj=project)\n    assert result.exit_code == 0\n\n    lines = result.output.splitlines()\n    assert \"Files: 4\" in lines[4]\n    assert \"Files: 4\" in lines[6]\n\n\n@pytest.mark.parametrize(\n    \"url,hash\",\n    [\n        (\n            \"http://fixtures.test/artifacts/demo-0.0.1.tar.gz\",\n            \"sha256:d57bf5e3b8723e4fc68275159dcc4ca983d86d4c84220a4d715d491401f27db2\",\n        ),\n        (\n            (FIXTURES / \"artifacts/demo-0.0.1.tar.gz\").as_uri(),\n            \"sha256:d57bf5e3b8723e4fc68275159dcc4ca983d86d4c84220a4d715d491401f27db2\",\n        ),\n        (\n            \"http://fixtures.test/artifacts/demo-0.0.1.tar.gz#sha384=9130e5e4912bc78b\"\n            \"1ffabbf406d56bc74b9165b0adc8c627168b7b563b80d5ff6c30e269398d01144ee52aa3\"\n            \"3292682d\",\n            \"sha384:9130e5e4912bc78b1ffabbf406d56bc74b9165b0adc8c627168b7b563b80d5ff6c30e269398d01144ee52aa33292682d\",\n        ),\n        (\n            \"http://fixtures.test/artifacts/demo-0.0.1.tar.gz#md5=5218509812c9fcb4646adde8fd3307e1\",\n            \"sha256:d57bf5e3b8723e4fc68275159dcc4ca983d86d4c84220a4d715d491401f27db2\",\n        ),\n    ],\n)\ndef test_hash_cache(project, url, hash):\n    hash_cache = project.make_hash_cache()\n    assert hash_cache.get_hash(Link(url), project.environment.session) == hash\n\n\ndef test_clear_package_cache(project, pdm):\n    pkg = CachedPackage(project.cache(\"packages\") / \"test_package.whl.cache\")\n    pkg.path.mkdir(parents=True)\n    refer_pkg = project.root / \"refer_pkg\"\n    refer_pkg.mkdir()\n    pkg.add_referrer(str(refer_pkg))\n    assert len(pkg.referrers) == 1\n\n    refer_pkg.rmdir()\n    pdm([\"cache\", \"clear\", \"packages\"], obj=project, strict=True)\n    assert not pkg.path.exists()\n"
  },
  {
    "path": "tests/cli/test_completion.py",
    "content": "\"\"\"Tests for the completion command\"\"\"\n\n\ndef test_completion_bash(pdm):\n    \"\"\"Test completion for bash shell\"\"\"\n    result = pdm([\"completion\", \"bash\"])\n    assert result.exit_code == 0\n    assert \"BASH completion script for pdm\" in result.output\n\n\ndef test_completion_zsh(pdm):\n    \"\"\"Test completion for zsh shell\"\"\"\n    result = pdm([\"completion\", \"zsh\"])\n    assert result.exit_code == 0\n    assert \"#compdef pdm\" in result.output\n\n\ndef test_completion_fish(pdm):\n    \"\"\"Test completion for fish shell\"\"\"\n    result = pdm([\"completion\", \"fish\"])\n    assert result.exit_code == 0\n    assert \"FISH completion script for pdm\" in result.output\n\n\ndef test_completion_powershell(pdm):\n    \"\"\"Test completion for powershell\"\"\"\n    result = pdm([\"completion\", \"powershell\"])\n    assert result.exit_code == 0\n    assert \"Powershell completion script for pdm\" in result.output\n\n\ndef test_completion_pwsh(pdm):\n    \"\"\"Test completion for pwsh (PowerShell Core)\"\"\"\n    result = pdm([\"completion\", \"pwsh\"])\n    assert result.exit_code == 0\n    assert \"Powershell completion script for pdm\" in result.output\n\n\ndef test_completion_unsupported_shell(pdm):\n    \"\"\"Test completion with unsupported shell raises error\"\"\"\n    result = pdm([\"completion\", \"unsupported_shell\"])\n    assert result.exit_code != 0\n    assert \"Unsupported shell\" in result.stderr\n\n\ndef test_completion_auto_detect(pdm, monkeypatch):\n    \"\"\"Test completion with auto-detected shell\"\"\"\n    import shellingham\n\n    monkeypatch.setattr(shellingham, \"detect_shell\", lambda: (\"bash\", \"/bin/bash\"))\n    result = pdm([\"completion\"])\n    assert result.exit_code == 0\n    assert \"BASH completion script for pdm\" in result.output\n\n\ndef test_completion_auto_detect_unsupported(pdm, monkeypatch):\n    \"\"\"Test completion with auto-detected unsupported shell\"\"\"\n    import shellingham\n\n    monkeypatch.setattr(shellingham, \"detect_shell\", lambda: (\"csh\", \"/bin/csh\"))\n    result = pdm([\"completion\"])\n    assert result.exit_code != 0\n    assert \"Unsupported shell\" in result.stderr\n"
  },
  {
    "path": "tests/cli/test_config.py",
    "content": "import pytest\n\nfrom pdm.exceptions import PdmUsageError\nfrom pdm.utils import cd\n\n\ndef test_config_command(project, pdm):\n    result = pdm([\"config\"], obj=project)\n    assert result.exit_code == 0\n    assert \"python.use_pyenv = True\" in result.output\n\n    result = pdm([\"config\", \"-v\"], obj=project)\n    assert result.exit_code == 0\n    assert \"Use the pyenv interpreter\" in result.output\n\n\ndef test_config_get_command(project, pdm):\n    result = pdm([\"config\", \"python.use_pyenv\"], obj=project)\n    assert result.exit_code == 0\n    assert result.output.strip() == \"True\"\n\n    result = pdm([\"config\", \"foo.bar\"], obj=project)\n    assert result.exit_code != 0\n\n\ndef test_config_set_command(project, pdm):\n    result = pdm([\"config\", \"python.use_pyenv\", \"false\"], obj=project)\n    assert result.exit_code == 0\n    result = pdm([\"config\", \"python.use_pyenv\"], obj=project)\n    assert result.output.strip() == \"False\"\n\n    result = pdm([\"config\", \"foo.bar\"], obj=project)\n    assert result.exit_code != 0\n\n    result = pdm([\"config\", \"-l\", \"cache_dir\", \"/path/to/bar\"], obj=project)\n    assert result.exit_code != 0\n\n\ndef test_config_del_command(project, pdm):\n    result = pdm([\"config\", \"-l\", \"python.use_pyenv\", \"false\"], obj=project)\n    assert result.exit_code == 0\n\n    result = pdm([\"config\", \"python.use_pyenv\"], obj=project)\n    assert result.output.strip() == \"False\"\n\n    result = pdm([\"config\", \"-ld\", \"python.use_pyenv\"], obj=project)\n    assert result.exit_code == 0\n\n    result = pdm([\"config\", \"python.use_pyenv\"], obj=project)\n    assert result.output.strip() == \"True\"\n\n\ndef test_config_env_var_shadowing(project, pdm, monkeypatch):\n    monkeypatch.setenv(\"PDM_PYPI_URL\", \"https://example.org/simple\")\n    result = pdm([\"config\", \"pypi.url\"], obj=project)\n    assert result.output.strip() == \"https://example.org/simple\"\n\n    result = pdm([\"config\", \"pypi.url\", \"https://test.pypi.org/pypi\"], obj=project)\n    assert \"config is shadowed by env var 'PDM_PYPI_URL'\" in result.stderr\n    result = pdm([\"config\", \"pypi.url\"], obj=project)\n    assert result.output.strip() == \"https://example.org/simple\"\n\n    monkeypatch.delenv(\"PDM_PYPI_URL\")\n    result = pdm([\"config\", \"pypi.url\"], obj=project)\n    assert result.output.strip() == \"https://test.pypi.org/pypi\"\n\n\ndef test_config_project_global_precedence(project, pdm):\n    pdm([\"config\", \"python.use_pyenv\", \"true\"], obj=project)\n    pdm([\"config\", \"-l\", \"python.use_pyenv\", \"false\"], obj=project)\n\n    result = pdm([\"config\", \"python.use_pyenv\"], obj=project)\n    assert result.output.strip() == \"False\"\n\n\ndef test_specify_config_file(tmp_path, pdm, monkeypatch):\n    tmp_path.joinpath(\"global_config.toml\").write_text(\"strategy.resolve_max_rounds = 1000\\n\")\n    with cd(tmp_path):\n        result = pdm([\"-c\", \"global_config.toml\", \"config\", \"strategy.resolve_max_rounds\"])\n        assert result.exit_code == 0\n        assert result.output.strip() == \"1000\"\n\n        monkeypatch.setenv(\"PDM_CONFIG_FILE\", \"global_config.toml\")\n        result = pdm([\"config\", \"strategy.resolve_max_rounds\"])\n        assert result.exit_code == 0\n        assert result.output.strip() == \"1000\"\n\n\ndef test_default_repository_setting(project):\n    repository = project.global_config.get_repository_config(\"pypi\", \"repository\")\n    assert repository.url == \"https://upload.pypi.org/legacy/\"\n    assert repository.username is None\n    assert repository.password is None\n\n    repository = project.global_config.get_repository_config(\"testpypi\", \"repository\")\n    assert repository.url == \"https://test.pypi.org/legacy/\"\n\n    repository = project.global_config.get_repository_config(\"nonexist\", \"repository\")\n    assert repository is None\n\n\ndef test_repository_config_key_short(project):\n    with pytest.raises(PdmUsageError):\n        project.global_config[\"repository.test\"] = {\"url\": \"https://example.org/simple\"}\n\n    with pytest.raises(PdmUsageError):\n        project.global_config[\"repository\"] = \"123\"\n\n    with pytest.raises(PdmUsageError):\n        del project.global_config[\"repository\"]\n\n\ndef test_repository_overwrite_default(project):\n    project.global_config[\"repository.pypi.username\"] = \"foo\"\n    project.global_config[\"repository.pypi.password\"] = \"bar\"\n    repository = project.global_config.get_repository_config(\"pypi\", \"repository\")\n    assert repository.url == \"https://upload.pypi.org/legacy/\"\n    assert repository.username == \"foo\"\n    assert repository.password == \"bar\"\n\n    project.global_config[\"repository.pypi.url\"] = \"https://example.pypi.org/legacy/\"\n    repository = project.global_config.get_repository_config(\"pypi\", \"repository\")\n    assert repository.url == \"https://example.pypi.org/legacy/\"\n\n\ndef test_hide_password_in_output_repository(project, pdm):\n    assert project.global_config[\"repository.pypi.password\"] is None\n    project.global_config[\"repository.pypi.username\"] = \"testuser\"\n    project.global_config[\"repository.pypi.password\"] = \"secret\"\n    result = pdm([\"config\", \"repository.pypi\"], obj=project, strict=True)\n    assert \"password = <hidden>\" in result.output\n    result = pdm([\"config\", \"repository.pypi.password\"], obj=project, strict=True)\n    assert \"<hidden>\" == result.output.strip()\n\n\ndef test_hide_password_in_output_pypi(project, pdm):\n    with pytest.raises(KeyError):\n        assert project.global_config[\"pypi.extra.password\"] is None\n    project.global_config[\"pypi.extra.username\"] = \"testuser\"\n    project.global_config[\"pypi.extra.password\"] = \"secret\"\n    project.global_config[\"pypi.extra.url\"] = \"https://test/simple\"\n    result = pdm([\"config\", \"pypi.extra\"], obj=project, strict=True)\n    assert \"password = <hidden>\" in result.output\n    result = pdm([\"config\", \"pypi.extra.password\"], obj=project, strict=True)\n    assert \"<hidden>\" == result.output.strip()\n    result = pdm([\"config\"], obj=project)\n    assert \"pypi.extra.password\" in result.output\n    assert \"<hidden>\" in result.output\n\n\ndef test_config_get_repository(project, pdm):\n    config = project.global_config[\"repository.pypi\"]\n    assert config == project.global_config.get_repository_config(\"pypi\", \"repository\")\n    assert project.global_config[\"repository.pypi.url\"] == \"https://upload.pypi.org/legacy/\"\n\n    result = pdm([\"config\", \"repository.pypi\"], obj=project, strict=True)\n    assert result.stdout.strip() == \"repository.pypi.url = https://upload.pypi.org/legacy/\"\n\n    assert (\n        project.global_config.get_repository_config(\"https://example.pypi.org/legacy/\", \"repository\").url\n        == \"https://example.pypi.org/legacy/\"\n    )\n\n    result = pdm([\"config\", \"repository.pypi.url\"], obj=project, strict=True)\n    assert result.stdout.strip() == \"https://upload.pypi.org/legacy/\"\n\n\ndef test_config_set_repository(project):\n    project.global_config[\"repository.pypi.url\"] = \"https://example.pypi.org/legacy/\"\n    project.global_config[\"repository.pypi.username\"] = \"foo\"\n    assert project.global_config[\"repository.pypi.url\"] == \"https://example.pypi.org/legacy/\"\n    assert project.global_config[\"repository.pypi.username\"] == \"foo\"\n    del project.global_config[\"repository.pypi.username\"]\n    assert project.global_config[\"repository.pypi.username\"] is None\n\n\ndef test_config_del_repository(project):\n    project.global_config[\"repository.test.url\"] = \"https://example.org/simple\"\n    assert project.global_config.get_repository_config(\"test\", \"repository\") is not None\n\n    del project.global_config[\"repository.test\"]\n    assert project.global_config.get_repository_config(\"test\", \"repository\") is None\n\n\ndef test_config_password_save_into_keyring(project, keyring):\n    project.global_config.update(\n        {\n            \"pypi.extra.url\": \"https://extra.pypi.org/simple\",\n            \"pypi.extra.username\": \"foo\",\n            \"pypi.extra.password\": \"barbaz\",\n            \"repository.pypi.username\": \"frost\",\n            \"repository.pypi.password\": \"password\",\n        }\n    )\n\n    assert project.global_config[\"pypi.extra.password\"] == \"barbaz\"\n    assert project.global_config[\"repository.pypi.password\"] == \"password\"\n    for key in (\"pypi.extra\", \"repository.pypi\"):\n        assert \"password\" not in project.global_config._file_data[key]\n\n    assert keyring.enabled\n    assert keyring.get_auth_info(\"pdm-pypi-extra\", \"foo\") == (\"foo\", \"barbaz\")\n    assert keyring.get_auth_info(\"pdm-repository-pypi\", None) == (\"frost\", \"password\")\n\n    del project.global_config[\"pypi.extra\"]\n    del project.global_config[\"repository.pypi.password\"]\n    keyring.get_auth_info.cache_clear()\n    assert keyring.get_auth_info(\"pdm-pypi-extra\", \"foo\") is None\n    assert keyring.get_auth_info(\"pdm-repository-pypi\", None) is None\n\n\ndef test_keyring_operation_error_disables_itself(project, keyring, mocker):\n    saver = mocker.patch.object(keyring.provider, \"save_auth_info\", side_effect=RuntimeError())\n    getter = mocker.patch.object(keyring.provider, \"get_auth_info\")\n    project.global_config.update(\n        {\n            \"pypi.extra.url\": \"https://extra.pypi.org/simple\",\n            \"pypi.extra.username\": \"foo\",\n            \"pypi.extra.password\": \"barbaz\",\n            \"repository.pypi.username\": \"frost\",\n            \"repository.pypi.password\": \"password\",\n        }\n    )\n\n    assert project.global_config[\"pypi.extra.password\"] == \"barbaz\"\n    assert project.global_config[\"repository.pypi.password\"] == \"password\"\n\n    saver.assert_called_once()\n    getter.assert_not_called()\n\n    assert not keyring.enabled\n    assert keyring.get_auth_info(\"pdm-pypi-extra\", \"foo\") is None\n    assert keyring.get_auth_info(\"pdm-repository-pypi\", None) is None\n"
  },
  {
    "path": "tests/cli/test_fix.py",
    "content": "import sys\nfrom pathlib import Path\n\n\ndef test_fix_non_existing_problem(project, pdm):\n    result = pdm([\"fix\", \"non-existing\"], obj=project)\n    assert result.exit_code == 1\n\n\ndef test_fix_individual_problem(project, pdm):\n    project._saved_python = None\n    old_config = project.root / \".pdm.toml\"\n    old_config.write_text(f'[python]\\nuse_pyenv = false\\npath = \"{Path(sys.executable).as_posix()}\"\\n')\n    pdm([\"fix\", \"project-config\"], obj=project, strict=True)\n    assert not old_config.exists()\n\n\ndef test_show_fix_command(project, pdm):\n    old_config = project.root / \".pdm.toml\"\n    old_config.write_text(f'[python]\\nuse_pyenv = false\\npath = \"{Path(sys.executable).as_posix()}\"\\n')\n    result = pdm([\"info\"], obj=project)\n    assert \"Run pdm fix to fix all\" in result.stderr\n\n    result = pdm([\"fix\", \"-h\"], obj=project)\n    assert \"Run pdm fix to fix all\" not in result.stderr\n\n\ndef test_show_fix_command_global_project(core, pdm, project_no_init):\n    project = core.create_project(None, True, project_no_init.global_config.config_file)\n    old_config = project.root / \".pdm.toml\"\n    old_config.write_text(f'[python]\\nuse_pyenv = false\\npath = \"{Path(sys.executable).as_posix()}\"\\n')\n    result = pdm([\"info\"], obj=project)\n    assert \"Run pdm fix -g to fix all\" in result.stderr\n\n    result = pdm([\"fix\", \"-h\"], obj=project)\n    assert \"Run pdm fix -g to fix all\" not in result.stderr\n\n\ndef test_fix_project_config(project, pdm):\n    project._saved_python = None\n    old_config = project.root / \".pdm.toml\"\n    old_config.write_text(f'[python]\\nuse_pyenv = false\\npath = \"{Path(sys.executable).as_posix()}\"\\n')\n    assert project.project_config[\"python.use_pyenv\"] is False\n    assert project._saved_python == Path(sys.executable).as_posix()\n    pdm([\"fix\"], obj=project, strict=True)\n    assert not old_config.exists()\n    assert project.root.joinpath(\"pdm.toml\").read_text() == \"[python]\\nuse_pyenv = false\\n\"\n    assert project.root.joinpath(\".pdm-python\").read_text().strip() == Path(sys.executable).as_posix()\n"
  },
  {
    "path": "tests/cli/test_hooks.py",
    "content": "import shlex\nimport sys\nfrom collections import namedtuple\nfrom textwrap import dedent\n\nimport pytest\n\nfrom pdm.cli import actions\nfrom pdm.cli.options import from_splitted_env\nfrom pdm.signals import pdm_signals\n\npytestmark = pytest.mark.usefixtures(\"repository\", \"working_set\", \"local_finder\")\n\n\ndef test_pre_script_fail_fast(project, pdm, capfd, mocker):\n    project.pyproject.settings[\"scripts\"] = {\n        \"pre_install\": \"python -c \\\"print('PRE INSTALL CALLED'); exit(1)\\\"\",\n        \"post_install\": \"python -c \\\"print('POST INSTALL CALLED')\\\"\",\n    }\n    project.pyproject.write()\n    synchronize = mocker.patch(\"pdm.installers.synchronizers.Synchronizer.synchronize\")\n    result = pdm([\"install\"], obj=project)\n    assert result.exit_code == 1\n    out, _ = capfd.readouterr()\n    assert \"PRE INSTALL CALLED\" in out\n    assert \"POST INSTALL CALLED\" not in out\n    synchronize.assert_not_called()\n\n\ndef test_pre_and_post_scripts(project, pdm, capfd, _echo):\n    project.pyproject.settings[\"scripts\"] = {\n        \"pre_script\": \"python echo.py pre_script\",\n        \"post_script\": \"python echo.py post_script\",\n        \"pre_test\": \"python echo.py pre_test\",\n        \"test\": \"python echo.py test\",\n        \"post_test\": \"python echo.py post_test\",\n        \"pre_run\": \"python echo.py pre_run\",\n        \"post_run\": \"python echo.py post_run\",\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", \"test\"], strict=True, obj=project)\n    out, _ = capfd.readouterr()\n    expected = dedent(\n        \"\"\"\n        pre_run CALLED\n        pre_script CALLED\n        pre_test CALLED\n        test CALLED\n        post_test CALLED\n        post_script CALLED\n        post_run CALLED\n        \"\"\"\n    ).strip()\n    assert out.strip() == expected\n\n\ndef test_composite_runs_all_hooks(project, pdm, capfd, _echo):\n    project.pyproject.settings[\"scripts\"] = {\n        \"test\": {\"composite\": [\"first\", \"second\"]},\n        \"pre_test\": \"python echo.py Pre-Test\",\n        \"post_test\": \"python echo.py Post-Test\",\n        \"first\": \"python echo.py First\",\n        \"pre_first\": \"python echo.py Pre-First\",\n        \"second\": \"python echo.py Second\",\n        \"post_second\": \"python echo.py Post-Second\",\n        \"pre_script\": \"python echo.py Pre-Script\",\n        \"post_script\": \"python echo.py Post-Script\",\n        \"pre_run\": \"python echo.py Pre-Run\",\n        \"post_run\": \"python echo.py Post-Run\",\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", \"test\"], strict=True, obj=project)\n    out, _ = capfd.readouterr()\n    expected = dedent(\n        \"\"\"\n        Pre-Run CALLED\n        Pre-Script CALLED\n        Pre-Test CALLED\n        Pre-Script CALLED\n        Pre-First CALLED\n        First CALLED\n        Post-Script CALLED\n        Pre-Script CALLED\n        Second CALLED\n        Post-Second CALLED\n        Post-Script CALLED\n        Post-Test CALLED\n        Post-Script CALLED\n        Post-Run CALLED\n        \"\"\"\n    ).strip()\n    assert out.strip() == expected\n\n\n@pytest.mark.parametrize(\"option\", [\":all\", \":pre,:post\"])\ndef test_skip_all_hooks_option(project, pdm, capfd, option: str, _echo):\n    project.pyproject.settings[\"scripts\"] = {\n        \"test\": {\"composite\": [\"first\", \"second\"]},\n        \"pre_test\": \"python echo.py Pre-Test\",\n        \"post_test\": \"python echo.py Post-Test\",\n        \"first\": \"python echo.py First\",\n        \"pre_first\": \"python echo.py Pre-First\",\n        \"post_first\": \"python echo.py Post-First\",\n        \"second\": \"python echo.py Second\",\n        \"pre_second\": \"python echo.py Pre-Second\",\n        \"post_second\": \"python echo.py Post-Second\",\n        \"pre_script\": \"python echo.py Pre-Script\",\n        \"post_script\": \"python echo.py Post-Script\",\n        \"pre_run\": \"python echo.py Pre-Run\",\n        \"post_run\": \"python echo.py Post-Run\",\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", f\"--skip={option}\", \"first\"], strict=True, obj=project)\n    out, _ = capfd.readouterr()\n    assert \"Pre-First CALLED\" not in out\n    assert \"First CALLED\" in out\n    assert \"Post-First CALLED\" not in out\n    assert \"Pre-Script CALLED\" not in out\n    assert \"Post-Script CALLED\" not in out\n    capfd.readouterr()\n    pdm([\"run\", f\"--skip={option}\", \"test\"], strict=True, obj=project)\n    out, _ = capfd.readouterr()\n    assert \"Pre-Test CALLED\" not in out\n    assert \"Pre-First CALLED\" not in out\n    assert \"First CALLED\" in out\n    assert \"Post-First CALLED\" not in out\n    assert \"Pre-Second CALLED\" not in out\n    assert \"Second CALLED\" in out\n    assert \"Post-Second CALLED\" not in out\n    assert \"Post-Test CALLED\" not in out\n    assert \"Pre-Script CALLED\" not in out\n    assert \"Post-Script CALLED\" not in out\n    assert \"Pre-Run CALLED\" not in out\n    assert \"Post-Run CALLED\" not in out\n\n\n@pytest.mark.parametrize(\n    \"args\",\n    [\n        \"--skip pre_test,post_first,second\",\n        \"-k pre_test,post_first,second\",\n        \"--skip pre_test --skip post_first --skip second\",\n        \"-k pre_test -k post_first -k second\",\n        \"--skip pre_test --skip post_first,second\",\n        \"-k pre_test -k post_first,second\",\n    ],\n)\ndef test_skip_option(project, pdm, capfd, args, _echo):\n    project.pyproject.settings[\"scripts\"] = {\n        \"test\": {\"composite\": [\"first\", \"second\"]},\n        \"pre_test\": \"python echo.py Pre-Test\",\n        \"post_test\": \"python echo.py Post-Test\",\n        \"first\": \"python echo.py First\",\n        \"pre_first\": \"python echo.py Pre-First\",\n        \"post_first\": \"python echo.py Post-First\",\n        \"second\": \"python echo.py Second\",\n        \"pre_second\": \"python echo.py Pre-Second\",\n        \"post_second\": \"python echo.py Post-Second\",\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", *shlex.split(args), \"test\"], strict=True, obj=project)\n    out, _ = capfd.readouterr()\n    assert \"Pre-Test CALLED\" not in out\n    assert \"Pre-First CALLED\" in out\n    assert \"First CALLED\" in out\n    assert \"Post-First CALLED\" not in out\n    assert \"Pre-Second CALLED\" not in out\n    assert \"Second CALLED\" not in out\n    assert \"Post-Second CALLED\" not in out\n    assert \"Post-Test CALLED\" in out\n\n\n@pytest.mark.parametrize(\n    \"env, expected\",\n    [\n        (\"pre_test\", [\"pre_test\"]),\n        (\"pre_test,post_test\", [\"pre_test\", \"post_test\"]),\n        (\"pre_test , post_test\", [\"pre_test\", \"post_test\"]),\n        (None, None),\n        (\" \", None),\n        (\" , \", None),\n    ],\n)\ndef test_skip_option_default_from_env(env, expected, monkeypatch):\n    if env is not None:\n        monkeypatch.setenv(\"PDM_SKIP_HOOKS\", env)\n\n    # Default value is set once and not easily testable\n    # so we test the function generating this default value\n    assert from_splitted_env(\"PDM_SKIP_HOOKS\", \",\") == expected\n\n\nHookSpecs = namedtuple(\"HookSpecs\", [\"command\", \"hooks\", \"fixtures\"])\nthis_python_version = f\"{sys.version_info[0]}.{sys.version_info[1]}\"\n\nKNOWN_COMMAND_HOOKS = (\n    (\"add\", \"add requests\", (\"pre_lock\", \"post_lock\"), [\"working_set\"]),\n    (\"build\", \"build\", (\"pre_build\", \"post_build\"), []),\n    (\"init\", \"init --non-interactive\", (\"post_init\",), []),\n    (\n        \"install\",\n        \"install\",\n        (\"pre_install\", \"post_install\", \"pre_lock\", \"post_lock\"),\n        [\"repository\"],\n    ),\n    (\"lock\", \"lock\", (\"pre_lock\", \"post_lock\"), []),\n    (\n        \"publish\",\n        \"publish --username abc --password 123\",\n        (\"pre_publish\", \"pre_build\", \"post_build\", \"post_publish\"),\n        [\"mock_publish\"],\n    ),\n    (\"remove\", \"remove requests\", (\"pre_lock\", \"post_lock\"), [\"lock\"]),\n    (\"sync\", \"sync\", (\"pre_install\", \"post_install\"), [\"lock\"]),\n    (\"update\", \"update\", (\"pre_install\", \"post_install\", \"pre_lock\", \"post_lock\"), []),\n    (\"use\", f\"use -f {this_python_version}\", (\"post_use\",), []),\n)\n\nparametrize_with_commands = pytest.mark.parametrize(\n    \"specs\",\n    [pytest.param(HookSpecs(command, hooks, fixtures), id=id) for id, command, hooks, fixtures in KNOWN_COMMAND_HOOKS],\n)\n\nparametrize_with_hooks = pytest.mark.parametrize(\n    \"specs,hook\",\n    [\n        pytest.param(HookSpecs(command, hooks, fixtures), hook, id=f\"{id}-{hook}\")\n        for id, command, hooks, fixtures in KNOWN_COMMAND_HOOKS\n        for hook in hooks\n    ],\n)\n\n\n@pytest.fixture\ndef hooked_project(project, capfd, specs, request):\n    project.pyproject.settings[\"scripts\"] = {hook: f\"python -c \\\"print('{hook} CALLED')\\\"\" for hook in pdm_signals}\n    project.pyproject.write()\n    for fixture in specs.fixtures:\n        request.getfixturevalue(fixture)\n    capfd.readouterr()\n    return project\n\n\n@pytest.fixture\ndef lock(project, capfd):\n    project.add_dependencies([\"requests\"])\n    actions.do_lock(project)\n    capfd.readouterr()\n\n\n@parametrize_with_commands\ndef test_hooks(hooked_project, pdm, capfd, specs: HookSpecs):\n    pdm(shlex.split(specs.command), strict=True, obj=hooked_project)\n    out, _ = capfd.readouterr()\n    for hook in specs.hooks:\n        assert f\"{hook} CALLED\" in out\n\n\n@parametrize_with_hooks  # Iterate over hooks as we need a clean slate for each run\ndef test_skip_option_from_signal(hooked_project, pdm, capfd, specs: HookSpecs, hook: str):\n    pdm([*shlex.split(specs.command), f\"--skip={hook}\"], strict=True, obj=hooked_project)\n    out, _ = capfd.readouterr()\n    assert f\"{hook} CALLED\" not in out\n    for known_hook in specs.hooks:\n        if known_hook != hook:\n            assert f\"{known_hook} CALLED\" in out\n\n\n@parametrize_with_commands\n@pytest.mark.parametrize(\"option\", [\":all\", \":pre,:post\"])\ndef test_skip_all_option_from_signal(hooked_project, pdm, capfd, specs: HookSpecs, option: str):\n    pdm(\n        [*shlex.split(specs.command), f\"--skip={option}\"],\n        strict=True,\n        obj=hooked_project,\n    )\n    out, _ = capfd.readouterr()\n    for hook in pdm_signals:\n        assert f\"{hook} CALLED\" not in out\n\n\n@parametrize_with_commands\n@pytest.mark.parametrize(\"prefix\", [\"pre\", \"post\"])\ndef test_skip_pre_post_option_from_signal(hooked_project, pdm, capfd, specs: HookSpecs, prefix: str):\n    pdm(\n        [*shlex.split(specs.command), f\"--skip=:{prefix}\"],\n        strict=True,\n        obj=hooked_project,\n    )\n    out, _ = capfd.readouterr()\n    for hook in specs.hooks:\n        if hook.startswith(prefix):\n            assert f\"{hook} CALLED\" not in out\n        else:\n            assert f\"{hook} CALLED\" in out\n"
  },
  {
    "path": "tests/cli/test_info.py",
    "content": "\"\"\"Additional tests for the info command to improve coverage\"\"\"\n\nimport json\n\n\ndef test_info_command_packages_option(project, pdm):\n    \"\"\"Test info command with --packages option\"\"\"\n    result = pdm([\"info\", \"--packages\"], obj=project)\n    assert result.exit_code == 0\n    # Should show packages path\n    assert result.output.strip() != \"\"\n\n\ndef test_info_command_all_options_mutually_exclusive(project, pdm):\n    \"\"\"Test that info command options are mutually exclusive\"\"\"\n    # Only one field option should work at a time\n    result = pdm([\"info\", \"--python\"], obj=project)\n    assert result.exit_code == 0\n\n    result = pdm([\"info\", \"--where\"], obj=project)\n    assert result.exit_code == 0\n\n    result = pdm([\"info\", \"--packages\"], obj=project)\n    assert result.exit_code == 0\n\n    result = pdm([\"info\", \"--env\"], obj=project)\n    assert result.exit_code == 0\n\n\ndef test_info_command_env_output_format(project, pdm):\n    \"\"\"Test that --env outputs valid JSON\"\"\"\n    result = pdm([\"info\", \"--env\"], obj=project, strict=True)\n    # Try to parse as JSON\n    try:\n        data = json.loads(result.output)\n        assert isinstance(data, dict)\n    except json.JSONDecodeError:\n        # If it's not JSON, it should at least contain marker info\n        assert \"python_version\" in result.output or \"implementation\" in result.output\n\n\ndef test_info_command_json_contains_all_fields(project, pdm):\n    \"\"\"Test that --json output contains all expected fields\"\"\"\n    result = pdm([\"info\", \"--json\"], obj=project, strict=True)\n\n    data = json.loads(result.output)\n\n    # Check pdm section\n    assert \"pdm\" in data\n    assert \"version\" in data[\"pdm\"]\n\n    # Check python section\n    assert \"python\" in data\n    assert \"interpreter\" in data[\"python\"]\n    assert \"version\" in data[\"python\"]\n    assert \"markers\" in data[\"python\"]\n    assert isinstance(data[\"python\"][\"markers\"], dict)\n\n    # Check project section\n    assert \"project\" in data\n    assert \"root\" in data[\"project\"]\n    assert \"pypackages\" in data[\"project\"]\n\n\ndef test_info_command_default_output(project, pdm):\n    \"\"\"Test info command with no options shows formatted output\"\"\"\n    result = pdm([\"info\"], obj=project, strict=True)\n\n    # Should contain all major sections\n    assert \"PDM version\" in result.output or \"PDM\" in result.output\n    assert \"Python Interpreter\" in result.output or \"Interpreter\" in result.output\n    assert \"Project Root\" in result.output or \"Root\" in result.output\n    assert \"Local Packages\" in result.output or \"Packages\" in result.output\n\n\ndef test_info_command_with_global_project(pdm, tmp_path, monkeypatch):\n    \"\"\"Test info command with global project\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    result = pdm([\"info\", \"-g\", \"--python\"])\n    assert result.exit_code == 0\n\n\ndef test_info_command_shows_venv_info(project, pdm):\n    \"\"\"Test info command shows virtual environment information when applicable\"\"\"\n    # Create a venv\n    project.global_config[\"python.use_venv\"] = True\n\n    result = pdm([\"info\"], obj=project)\n    assert result.exit_code == 0\n\n\ndef test_info_command_non_local_environment(project, pdm, mocker):\n    \"\"\"Test info command with non-local environment shows site-packages\"\"\"\n    # Mock the environment to be non-local\n    project.environment.is_local = False\n\n    # Mock get_paths to return purelib\n    project.environment.get_paths = mocker.Mock(return_value={\"purelib\": \"/fake/purelib\"})\n\n    result = pdm([\"info\", \"--packages\"], obj=project)\n    assert result.exit_code == 0\n    assert \"/fake/purelib\" in result.output or \"purelib\" in result.output\n\n\ndef test_info_command_global_project_prefix(project, pdm):\n    \"\"\"Test that global project shows 'Global' prefix in output\"\"\"\n    result = pdm([\"info\", \"-g\"], obj=project)\n\n    # If it's a global project, should show \"Global\" prefix\n    # This test checks if the code path exists\n    assert result.exit_code == 0\n"
  },
  {
    "path": "tests/cli/test_init.py",
    "content": "import sys\nfrom unittest.mock import ANY\n\nimport pytest\n\nfrom pdm.compat import tomllib\nfrom pdm.models.python import PythonInfo\nfrom pdm.utils import cd\n\nPYTHON_VERSION = f\"{sys.version_info[0]}.{sys.version_info[1]}\"\n\n\n@pytest.fixture(autouse=True)\ndef enable_interactive_mode(mocker):\n    from rich import get_console\n\n    console = get_console()\n    mocker.patch.object(console, \"is_interactive\", True)\n\n\ndef test_init_validate_python_requires(project_no_init, pdm):\n    result = pdm([\"init\"], input=\"\\n\\n\\n\\n\\n\\n\\n3.7\\n\", obj=project_no_init)\n    assert result.exit_code != 0\n    assert \"InvalidSpecifier\" in result.stderr\n\n\ndef test_init_command(project_no_init, pdm, mocker):\n    mocker.patch(\n        \"pdm.cli.commands.init.get_user_email_from_git\",\n        return_value=(\"Testing\", \"me@example.org\"),\n    )\n    pdm([\"init\"], input=\"\\ntest-project\\n\\n\\n\\n\\n\\n\\n\", strict=True, obj=project_no_init)\n    python_version = f\"{project_no_init.python.major}.{project_no_init.python.minor}\"\n    data = {\n        \"project\": {\n            \"authors\": [{\"email\": \"me@example.org\", \"name\": \"Testing\"}],\n            \"dependencies\": [],\n            \"description\": \"Default template for PDM package\",\n            \"license\": {\"text\": \"MIT\"},\n            \"name\": \"test-project\",\n            \"requires-python\": f\"=={python_version}.*\",\n            \"readme\": \"README.md\",\n            \"version\": \"0.1.0\",\n        },\n        \"tool\": {\"pdm\": {\"distribution\": False}},\n    }\n\n    with open(project_no_init.root.joinpath(\"pyproject.toml\"), \"rb\") as fp:\n        assert tomllib.load(fp) == data\n\n\ndef test_new_command(project_no_init, pdm, mocker):\n    mocker.patch(\n        \"pdm.cli.commands.init.get_user_email_from_git\",\n        return_value=(\"Testing\", \"me@example.org\"),\n    )\n    with cd(project_no_init.root):\n        pdm([\"new\", \"myproject\"], input=\"\\ntest-project\\n\\n\\n\\n\\n\\n\\n\", strict=True)\n    python_version = f\"{project_no_init.python.major}.{project_no_init.python.minor}\"\n    data = {\n        \"project\": {\n            \"authors\": [{\"email\": \"me@example.org\", \"name\": \"Testing\"}],\n            \"dependencies\": [],\n            \"description\": \"Default template for PDM package\",\n            \"license\": {\"text\": \"MIT\"},\n            \"name\": \"test-project\",\n            \"requires-python\": f\"=={python_version}.*\",\n            \"readme\": \"README.md\",\n            \"version\": \"0.1.0\",\n        },\n        \"tool\": {\"pdm\": {\"distribution\": False}},\n    }\n\n    with open(project_no_init.root.joinpath(\"myproject/pyproject.toml\"), \"rb\") as fp:\n        assert tomllib.load(fp) == data\n\n\ndef test_init_command_library(project_no_init, pdm, mocker):\n    mocker.patch(\n        \"pdm.cli.commands.init.get_user_email_from_git\",\n        return_value=(\"Testing\", \"me@example.org\"),\n    )\n    result = pdm(\n        [\"init\"],\n        input=\"\\ntest-project\\n\\ny\\nTest Project\\n1\\n\\n\\n\\n\\n\",\n        obj=project_no_init,\n    )\n    assert result.exit_code == 0\n    python_version = f\"{project_no_init.python.major}.{project_no_init.python.minor}\"\n    data = {\n        \"project\": {\n            \"authors\": [{\"email\": \"me@example.org\", \"name\": \"Testing\"}],\n            \"dependencies\": [],\n            \"description\": \"Test Project\",\n            \"license\": {\"text\": \"MIT\"},\n            \"name\": \"test-project\",\n            \"requires-python\": f\">={python_version}\",\n            \"readme\": \"README.md\",\n            \"version\": \"0.1.0\",\n        },\n        \"build-system\": {\"build-backend\": \"setuptools.build_meta\", \"requires\": [\"setuptools>=61\"]},\n        \"tool\": {\"pdm\": {\"distribution\": True}},\n    }\n\n    with open(project_no_init.root.joinpath(\"pyproject.toml\"), \"rb\") as fp:\n        assert tomllib.load(fp) == data\n\n\ndef test_init_non_interactive(project_no_init, pdm, mocker):\n    mocker.patch(\n        \"pdm.cli.commands.init.get_user_email_from_git\",\n        return_value=(\"Testing\", \"me@example.org\"),\n    )\n    do_use = mocker.patch(\"pdm.cli.commands.use.Command.do_use\", return_value=PythonInfo.from_path(sys.executable))\n    result = pdm([\"init\", \"-n\"], obj=project_no_init)\n    assert result.exit_code == 0\n    python_version = f\"{project_no_init.python.major}.{project_no_init.python.minor}\"\n    do_use.assert_called_once_with(\n        project_no_init,\n        ANY,\n        first=True,\n        ignore_remembered=True,\n        ignore_requires_python=True,\n        save=False,\n        hooks=ANY,\n    )\n    data = {\n        \"project\": {\n            \"authors\": [{\"email\": \"me@example.org\", \"name\": \"Testing\"}],\n            \"dependencies\": [],\n            \"description\": \"Default template for PDM package\",\n            \"license\": {\"text\": \"MIT\"},\n            \"name\": project_no_init.root.name,\n            \"requires-python\": f\"=={python_version}.*\",\n            \"readme\": \"README.md\",\n            \"version\": \"0.1.0\",\n        },\n        \"tool\": {\"pdm\": {\"distribution\": False}},\n    }\n\n    with open(project_no_init.root.joinpath(\"pyproject.toml\"), \"rb\") as fp:\n        assert tomllib.load(fp) == data\n\n\ndef test_init_auto_create_venv(project_no_init, pdm, mocker):\n    mocker.patch(\"pdm.models.python.PythonInfo.get_venv\", return_value=None)\n    project_no_init.project_config[\"python.use_venv\"] = True\n    result = pdm([\"init\"], input=\"\\ntest-project\\n\\ny\\nTest Project\\n1\\n\\n\\n\\n\\n\", obj=project_no_init)\n    assert result.exit_code == 0\n    assert project_no_init.python.executable.parent.parent == project_no_init.root / \".venv\"\n    assert \".pdm-python\" in (project_no_init.root / \".gitignore\").read_text()\n\n\ndef test_init_auto_create_venv_specify_python(project_no_init, pdm, mocker):\n    mocker.patch(\"pdm.models.python.PythonInfo.get_venv\", return_value=None)\n    project_no_init.project_config[\"python.use_venv\"] = True\n    result = pdm(\n        [\"init\", f\"--python={PYTHON_VERSION}\"],\n        input=\"\\n\\n\\n\\n\\n\\n\\n\",\n        obj=project_no_init,\n    )\n    assert result.exit_code == 0\n    assert project_no_init.python.executable.parent.parent == project_no_init.root / \".venv\"\n\n\ndef test_init_with_backend_default_library(project_no_init, pdm):\n    pdm([\"init\", \"--backend\", \"flit-core\"], input=\"\\n\\n\\n\\n\\n\\n\\n\\n\\n\", obj=project_no_init)\n    assert project_no_init.backend.__class__.__name__ == \"FlitBackend\"\n\n\ndef test_init_with_backend_default_library_non_interactive(project_no_init, pdm):\n    pdm([\"init\", \"-n\", \"--backend\", \"flit-core\"], obj=project_no_init)\n    assert project_no_init.backend.__class__.__name__ == \"FlitBackend\"\n\n\ndef test_init_with_license_non_interactive(project_no_init, pdm, mocker):\n    mocker.patch(\n        \"pdm.cli.commands.init.get_user_email_from_git\",\n        return_value=(\"Testing\", \"me@example.org\"),\n    )\n    do_use = mocker.patch(\"pdm.cli.commands.use.Command.do_use\", return_value=PythonInfo.from_path(sys.executable))\n    expected_license = \"Proprietary\"\n    result = pdm([\"init\", \"-n\", \"--license\", expected_license], obj=project_no_init)\n    assert result.exit_code == 0\n    python_version = f\"{project_no_init.python.major}.{project_no_init.python.minor}\"\n    do_use.assert_called_once_with(\n        project_no_init,\n        ANY,\n        first=True,\n        ignore_remembered=True,\n        ignore_requires_python=True,\n        save=False,\n        hooks=ANY,\n    )\n    data = {\n        \"project\": {\n            \"authors\": [{\"email\": \"me@example.org\", \"name\": \"Testing\"}],\n            \"dependencies\": [],\n            \"description\": \"Default template for PDM package\",\n            \"license\": {\"text\": f\"{expected_license}\"},\n            \"name\": project_no_init.root.name,\n            \"requires-python\": f\"=={python_version}.*\",\n            \"readme\": \"README.md\",\n            \"version\": \"0.1.0\",\n        },\n        \"tool\": {\"pdm\": {\"distribution\": False}},\n    }\n\n    with open(project_no_init.root.joinpath(\"pyproject.toml\"), \"rb\") as fp:\n        assert tomllib.load(fp) == data\n\n\ndef test_init_with_project_version_non_interactive(project_no_init, pdm, mocker):\n    mocker.patch(\n        \"pdm.cli.commands.init.get_user_email_from_git\",\n        return_value=(\"Testing\", \"me@example.org\"),\n    )\n    do_use = mocker.patch(\"pdm.cli.commands.use.Command.do_use\", return_value=PythonInfo.from_path(sys.executable))\n    expected_project_version = \"2.0.42\"\n    result = pdm([\"init\", \"-n\", \"--project-version\", expected_project_version], obj=project_no_init)\n    assert result.exit_code == 0\n    python_version = f\"{project_no_init.python.major}.{project_no_init.python.minor}\"\n    do_use.assert_called_once_with(\n        project_no_init,\n        ANY,\n        first=True,\n        ignore_remembered=True,\n        ignore_requires_python=True,\n        save=False,\n        hooks=ANY,\n    )\n    data = {\n        \"project\": {\n            \"authors\": [{\"email\": \"me@example.org\", \"name\": \"Testing\"}],\n            \"dependencies\": [],\n            \"description\": \"Default template for PDM package\",\n            \"license\": {\"text\": \"MIT\"},\n            \"name\": project_no_init.root.name,\n            \"requires-python\": f\"=={python_version}.*\",\n            \"readme\": \"README.md\",\n            \"version\": f\"{expected_project_version}\",\n        },\n        \"tool\": {\"pdm\": {\"distribution\": False}},\n    }\n\n    with open(project_no_init.root.joinpath(\"pyproject.toml\"), \"rb\") as fp:\n        assert tomllib.load(fp) == data\n"
  },
  {
    "path": "tests/cli/test_install.py",
    "content": "import pytest\n\nfrom pdm.cli import actions\nfrom pdm.models.markers import EnvSpec\nfrom pdm.pytest import Distribution\nfrom pdm.utils import cd\n\n\ndef test_sync_packages_with_group_all(project, working_set, pdm):\n    project.add_dependencies([\"requests\"])\n    project.add_dependencies([\"pytz\"], \"date\")\n    project.add_dependencies([\"pyopenssl\"], \"ssl\")\n    pdm([\"install\", \"-G:all\"], obj=project, strict=True)\n    assert \"pytz\" in working_set\n    assert \"requests\" in working_set\n    assert \"idna\" in working_set\n    assert \"pyopenssl\" in working_set\n\n\ndef test_sync_packages_with_all_dev(project, working_set, pdm):\n    project.add_dependencies([\"requests\"])\n    project.add_dependencies([\"pytz\"], \"date\", True)\n    project.add_dependencies([\"pyopenssl\"], \"ssl\", True)\n    pdm([\"install\", \"-d\", \"--no-default\"], obj=project, strict=True)\n    assert \"requests\" not in working_set\n    assert \"idna\" not in working_set\n    assert \"pytz\" in working_set\n    assert \"pyopenssl\" in working_set\n\n\ndef test_sync_no_lockfile(project, pdm):\n    project.add_dependencies([\"requests\"])\n    result = pdm([\"sync\"], obj=project)\n    assert result.exit_code == 1\n\n\ndef test_sync_clean_packages(project, working_set, pdm):\n    for candidate in [\n        Distribution(\"foo\", \"0.1.0\"),\n        Distribution(\"chardet\", \"3.0.1\"),\n        Distribution(\"idna\", \"2.7\"),\n    ]:\n        working_set.add_distribution(candidate)\n    pdm([\"add\", \"--no-sync\", \"requests\"], obj=project, strict=True)\n    pdm([\"sync\", \"--clean\"], obj=project, strict=True)\n    assert \"foo\" not in working_set\n\n\ndef test_sync_dry_run(project, working_set, pdm):\n    for candidate in [\n        Distribution(\"foo\", \"0.1.0\"),\n        Distribution(\"chardet\", \"3.0.1\"),\n        Distribution(\"idna\", \"2.7\"),\n    ]:\n        working_set.add_distribution(candidate)\n    pdm([\"add\", \"--no-sync\", \"requests\"], obj=project, strict=True)\n    pdm([\"sync\", \"--clean\", \"--dry-run\"], obj=project, strict=True)\n    assert \"foo\" in working_set\n    assert \"requests\" not in working_set\n    assert working_set[\"chardet\"].version == \"3.0.1\"\n\n\ndef test_sync_only_different(project, working_set, pdm):\n    working_set.add_distribution(Distribution(\"foo\", \"0.1.0\"))\n    working_set.add_distribution(Distribution(\"chardet\", \"3.0.1\"))\n    working_set.add_distribution(Distribution(\"idna\", \"2.7\"))\n    result = pdm([\"add\", \"requests\"], obj=project, strict=True)\n    out = result.stdout\n    assert \"3 to add\" in out, out\n    assert \"1 to update\" in out\n    assert \"foo\" in working_set\n    assert \"test-project\" in working_set, list(working_set)\n    assert working_set[\"chardet\"].version == \"3.0.4\"\n\n\ndef test_sync_in_sequential_mode(project, working_set, pdm):\n    project.project_config[\"install.parallel\"] = False\n    result = pdm([\"add\", \"requests\"], obj=project, strict=True)\n    assert \"5 to add\" in result.stdout\n    assert \"test-project\" in working_set\n    assert working_set[\"chardet\"].version == \"3.0.4\"\n\n\ndef test_sync_packages_with_groups(project, working_set, pdm):\n    project.add_dependencies([\"requests\"])\n    project.add_dependencies([\"pytz\"], \"date\")\n    pdm([\"install\", \"-Gdate\"], obj=project, strict=True)\n    assert \"pytz\" in working_set\n    assert \"requests\" in working_set\n    assert \"idna\" in working_set\n\n\n@pytest.mark.parametrize(\"prod_option\", [(\"--prod\",), ()])\ndef test_sync_production_packages(project, working_set, prod_option, pdm):\n    project.add_dependencies([\"requests\"])\n    project.add_dependencies([\"pytz\"], \"dev\", dev=True)\n    pdm([\"install\", *prod_option], obj=project, strict=True)\n    assert \"requests\" in working_set\n    assert (\"pytz\" in working_set) == (not prod_option)\n\n\ndef test_sync_without_self(project, working_set, pdm):\n    project.add_dependencies([\"requests\"])\n    pdm([\"install\", \"--no-self\"], obj=project, strict=True)\n    assert project.name not in working_set, list(working_set)\n\n\ndef test_sync_with_index_change(project, index, pdm):\n    project.project_config[\"pypi.url\"] = \"https://my.pypi.org/simple\"\n    project.pyproject.metadata[\"requires-python\"] = \">=3.6\"\n    project.pyproject.metadata[\"dependencies\"] = [\"future-fstrings\"]\n    project.pyproject.write()\n    index[\"/simple/future-fstrings/\"] = b\"\"\"\n    <html>\n    <body>\n        <h1>future-fstrings</h1>\n        <a href=\"http://fixtures.test/artifacts/future_fstrings-1.2.0-py2.py3-none-any\\\n.whl#sha256=90e49598b553d8746c4dc7d9442e0359d038c3039d802c91c0a55505da318c63\">\n        future_fstrings-1.2.0.tar.gz\n        </a>\n    </body>\n    </html>\n    \"\"\"\n    pdm([\"lock\"], obj=project, strict=True, cleanup=False)\n    file_hashes = project.lockfile[\"package\"][0][\"files\"]\n    assert [e[\"hash\"] for e in file_hashes] == [\n        \"sha256:90e49598b553d8746c4dc7d9442e0359d038c3039d802c91c0a55505da318c63\"\n    ]\n    # Mimic the CDN inconsistencies of PyPI simple index. See issues/596.\n    del index[\"/simple/future-fstrings/\"]\n    pdm([\"sync\", \"--no-self\"], obj=project, strict=True)\n\n\ndef test_install_command(project, pdm, mocker):\n    do_lock = mocker.patch.object(actions, \"do_lock\")\n    do_sync = mocker.patch.object(actions, \"do_sync\")\n    pdm([\"install\"], obj=project)\n    do_lock.assert_called_once()\n    do_sync.assert_called_once()\n\n\ndef test_sync_command(project, pdm, mocker):\n    pdm([\"lock\"], obj=project)\n    do_sync = mocker.patch.object(actions, \"do_sync\")\n    pdm([\"sync\"], obj=project)\n    do_sync.assert_called_once()\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_install_with_lockfile(project, pdm):\n    result = pdm([\"lock\", \"-v\"], obj=project)\n    assert result.exit_code == 0\n    result = pdm([\"install\"], obj=project)\n    assert \"Lockfile\" not in result.stderr\n\n    project.add_dependencies([\"pytz\"], \"default\")\n    result = pdm([\"install\"], obj=project)\n    assert \"Lockfile hash doesn't match\" in result.stderr\n    assert \"pytz\" in project.get_locked_repository().candidates\n    assert project.is_lockfile_hash_match()\n\n\ndef test_install_with_dry_run(project, pdm, repository):\n    project.add_dependencies([\"pytz\"], \"default\")\n    result = pdm([\"install\", \"--dry-run\"], obj=project)\n    project.lockfile.reload()\n    assert \"pytz\" not in project.get_locked_repository().candidates\n    assert \"pytz 2019.3\" in result.output\n\n\ndef test_install_frozen_lockfile(project, pdm, working_set):\n    project.add_dependencies([\"requests\"], \"default\")\n    result = pdm([\"install\", \"--frozen-lockfile\"], obj=project)\n    assert result.exit_code == 0\n    assert not project.lockfile.exists()\n    assert \"urllib3\" in working_set\n    assert \"requests\" in working_set\n\n\ndef test_install_no_lock_deprecated(project, pdm, working_set):\n    project.add_dependencies([\"requests\"], \"default\")\n    result = pdm([\"install\", \"--no-lock\"], obj=project)\n    assert result.exit_code == 0\n    assert not project.lockfile.exists()\n    assert \"urllib3\" in working_set\n    assert \"requests\" in working_set\n    assert \"WARNING: --no-lock is deprecated\" in result.stderr\n\n\ndef test_install_check(pdm, project, repository):\n    result = pdm([\"install\", \"--check\"], obj=project)\n    assert result.exit_code == 1\n\n    result = pdm([\"add\", \"requests\", \"--no-sync\"], obj=project)\n    project.add_dependencies([\"requests>=2.0\"])\n    result = pdm([\"install\", \"--check\"], obj=project)\n    assert result.exit_code == 1\n\n\ndef test_sync_with_clean_unselected_option(project, working_set, pdm):\n    project.add_dependencies([\"requests>=2.0\"])\n    project.add_dependencies([\"django\"], \"web\", True)\n    pdm([\"install\"], obj=project, strict=True)\n    assert all(p in working_set for p in (\"requests\", \"urllib3\", \"django\", \"pytz\")), list(working_set)\n    pdm([\"sync\", \"--prod\", \"--clean-unselected\"], obj=project, strict=True)\n    assert \"requests\" in working_set\n    assert \"urllib3\" in working_set\n    assert \"django\" not in working_set\n\n\ndef test_install_referencing_self_package(project, working_set, pdm):\n    project.add_dependencies([\"pytz\"], to_group=\"tz\")\n    project.add_dependencies([\"urllib3\"], to_group=\"web\")\n    project.add_dependencies([\"test-project[tz,web]\"], to_group=\"all\")\n    pdm([\"install\", \"-Gall\"], obj=project, strict=True)\n    assert \"pytz\" in working_set\n    assert \"urllib3\" in working_set\n\n\ndef test_install_monorepo_with_rel_paths(fixture_project, pdm, working_set):\n    project = fixture_project(\"test-monorepo\")\n    with cd(project.root):\n        pdm([\"install\"], obj=project, strict=True)\n    for package in (\"package-a\", \"package-b\", \"core\"):\n        assert package in working_set\n\n\n@pytest.mark.usefixtures(\"repository\")\ndef test_install_retry(project, pdm, mocker):\n    pdm([\"add\", \"certifi\", \"chardet\", \"--no-sync\"], obj=project)\n    handler = mocker.patch(\n        \"pdm.installers.synchronizers.Synchronizer.install_candidate\",\n        side_effect=RuntimeError,\n    )\n    result = pdm([\"install\"], obj=project)\n    assert result.exit_code == 1\n    handler.assert_has_calls(\n        [\n            mocker.call(\"certifi\", mocker.ANY),\n            mocker.call(\"chardet\", mocker.ANY),\n            mocker.call(\"certifi\", mocker.ANY),\n            mocker.call(\"chardet\", mocker.ANY),\n        ],\n        any_order=True,\n    )\n\n\n@pytest.mark.usefixtures(\"repository\")\ndef test_install_fail_fast(project, pdm, mocker):\n    project.project_config[\"install.parallel\"] = False\n    pdm([\"add\", \"certifi\", \"chardet\", \"pytz\", \"--no-sync\"], obj=project)\n\n    handler = mocker.patch(\n        \"pdm.installers.synchronizers.Synchronizer.install_candidate\",\n        side_effect=RuntimeError,\n    )\n    result = pdm([\"install\", \"--fail-fast\"], obj=project)\n    assert result.exit_code == 1\n    assert handler.call_count == 1\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_install_groups_not_in_lockfile(project, pdm):\n    project.add_dependencies([\"pytz\"], to_group=\"tz\")\n    project.add_dependencies([\"urllib3\"], to_group=\"web\")\n    pdm([\"install\", \"-vv\"], obj=project, strict=True)\n    assert project.lockfile.groups == [\"default\"]\n    all_locked_packages = project.get_locked_repository().candidates\n    for package in [\"pytz\", \"urllib3\"]:\n        assert package not in all_locked_packages\n    with pytest.raises(RuntimeError, match=\"Requested groups not in lockfile\"):\n        pdm([\"install\", \"-Gtz\"], obj=project, strict=True)\n\n\ndef test_install_locked_groups(project, pdm, working_set):\n    project.add_dependencies([\"urllib3\"])\n    project.add_dependencies([\"pytz\"], to_group=\"tz\")\n    pdm([\"lock\", \"-Gtz\", \"--no-default\"], obj=project, strict=True)\n    pdm([\"sync\"], obj=project, strict=True)\n    assert \"pytz\" in working_set\n    assert \"urllib3\" not in working_set\n\n\ndef test_install_groups_and_lock(project, pdm, working_set):\n    project.add_dependencies([\"urllib3\"])\n    project.add_dependencies([\"pytz\"], to_group=\"tz\")\n    pdm([\"install\", \"-Gtz\", \"--no-default\"], obj=project, strict=True)\n    assert \"pytz\" in working_set\n    assert \"urllib3\" not in working_set\n    assert project.lockfile.groups == [\"tz\"]\n    assert \"pytz\" in project.get_locked_repository().candidates\n    assert \"urllib3\" not in project.get_locked_repository().candidates\n\n\ndef test_install_requirement_with_extras(project, pdm, working_set):\n    project.add_dependencies([\"requests==2.19.1\"])\n    project.add_dependencies([\"requests[socks]\"], to_group=\"socks\")\n    pdm([\"lock\", \"-Gsocks\"], obj=project, strict=True)\n    pdm([\"sync\", \"-Gsocks\"], obj=project, strict=True)\n    assert \"pysocks\" in working_set\n\n\ndef test_fix_package_type_and_update(fixture_project, pdm, working_set):\n    project = fixture_project(\"test-package-type-fixer\")\n    pdm([\"fix\", \"package-type\"], obj=project, strict=True)\n    pdm([\"update\"], obj=project, strict=True)\n    assert \"test-package-type-fixer\" not in working_set\n\n\nexclusion_cases = [\n    pytest.param((\"-G\", \":all\", \"--without\", \"tz,ssl\"), id=\"-G :all --without tz,ssl\"),\n    pytest.param((\"-G\", \":all\", \"--without\", \"tz\", \"--without\", \"ssl\"), id=\"-G :all --without tz --without ssl\"),\n    pytest.param((\"--with\", \":all\", \"--without\", \"tz,ssl\"), id=\"--with all --without tz,ssl\"),\n    pytest.param((\"--with\", \":all\", \"--without\", \"tz\", \"--without\", \"ssl\"), id=\"--with all --without tz --without ssl\"),\n    pytest.param((\"--without\", \"tz\", \"--without\", \"ssl\"), id=\"--without tz --without ssl\"),\n    pytest.param((\"--without\", \"tz,ssl\"), id=\"--without tz,ssl\"),\n]\n\n\n@pytest.mark.parametrize(\"args\", exclusion_cases)\ndef test_install_all_with_excluded_groups(project, working_set, pdm, args):\n    project.add_dependencies([\"urllib3\"], \"url\")\n    project.add_dependencies([\"pytz\"], \"tz\", True)\n    project.add_dependencies([\"pyopenssl\"], \"ssl\")\n    pdm([\"install\", *args], obj=project, strict=True)\n    assert \"urllib3\" in working_set\n    assert \"pytz\" not in working_set\n    assert \"pyopenssl\" not in working_set\n\n\n@pytest.mark.parametrize(\"args\", exclusion_cases)\ndef test_sync_all_with_excluded_groups(project, working_set, pdm, args):\n    project.add_dependencies([\"urllib3\"], \"url\")\n    project.add_dependencies([\"pytz\"], \"tz\", True)\n    project.add_dependencies([\"pyopenssl\"], \"ssl\")\n    pdm([\"lock\", \"-G:all\"], obj=project, strict=True)\n    pdm([\"sync\", *args], obj=project, strict=True)\n    assert \"urllib3\" in working_set\n    assert \"pytz\" not in working_set\n    assert \"pyopenssl\" not in working_set\n\n\ndef test_excluded_groups_ignored_if_prod_passed(project, working_set, pdm):\n    project.add_dependencies([\"urllib3\"], \"url\")\n    project.add_dependencies([\"pytz\"], \"tz\")\n    project.add_dependencies([\"pyopenssl\"], \"ssl\")\n    pdm([\"install\", \"--prod\", \"--without\", \"ssl\"], obj=project, strict=True)\n    assert \"urllib3\" not in working_set\n    assert \"pytz\" not in working_set\n    assert \"pyopenssl\" not in working_set\n\n\ndef test_excluded_groups_ignored_if_dev_passed(project, working_set, pdm):\n    project.add_dependencies([\"urllib3\"], \"url\")\n    project.add_dependencies([\"pytz\"], \"tz\")\n    project.add_dependencies([\"pyopenssl\"], \"ssl\")\n    pdm([\"install\", \"--dev\", \"--without\", \"ssl\"], obj=project, strict=True)\n    assert \"urllib3\" not in working_set\n    assert \"pytz\" not in working_set\n    assert \"pyopenssl\" not in working_set\n\n\n@pytest.mark.parametrize(\"nested\", [False, True])\n@pytest.mark.parametrize(\"groups\", [(\"default\",), None])\ndef test_install_from_multi_target_lock(project, pdm, repository, nested, groups):\n    from pdm.cli.actions import resolve_candidates_from_lockfile\n\n    deps = [\n        'django<2; sys_platform == \"win32\"',\n        'django>=2; sys_platform != \"win32\"',\n    ]\n    if nested:\n        repository.add_candidate(\"foo\", \"0.1.0\")\n        repository.add_dependencies(\"foo\", \"0.1.0\", deps)\n        project.add_dependencies([\"foo\"])\n    else:\n        project.add_dependencies(deps)\n    pdm([\"lock\", \"--platform\", \"windows\"], obj=project, strict=True)\n    pdm([\"lock\", \"--platform\", \"linux\", \"--append\"], obj=project, strict=True)\n\n    candidates = resolve_candidates_from_lockfile(\n        project, project.get_dependencies(), env_spec=EnvSpec.from_spec(\"==3.11\", \"windows\"), groups=groups\n    )\n    assert candidates[\"django\"].version == \"1.11.8\"\n    assert \"sqlparse\" not in candidates\n\n    candidates = resolve_candidates_from_lockfile(\n        project, project.get_dependencies(), env_spec=EnvSpec.from_spec(\"==3.11\", \"linux\"), groups=groups\n    )\n    assert candidates[\"django\"].version == \"2.2.9\"\n    assert \"sqlparse\" in candidates\n\n\ndef test_install_from_lock_with_higher_version(project, pdm, working_set):\n    project.add_dependencies([\"django\"])\n    pdm([\"lock\", \"--platform\", \"manylinux_2_20_x86_64\"], obj=project, strict=True)\n    # Install with manylinux_2_17_x86_64 which is lower than the target\n    project.environment.__dict__[\"spec\"] = EnvSpec.from_spec(\"==3.11\", \"manylinux_2_17_x86_64\")\n    result = pdm([\"install\"], obj=project)\n    assert result.exit_code == 0\n    assert \"WARNING: Found lock target\" in result.stderr\n\n\ndef test_install_from_lock_with_lower_version(project, pdm, working_set):\n    project.add_dependencies([\"django\"])\n    pdm([\"lock\", \"--platform\", \"linux\"], obj=project, strict=True)\n    project.environment.__dict__[\"spec\"] = EnvSpec.from_spec(\"==3.11\", \"manylinux_2_20_x86_64\")\n    result = pdm([\"install\"], obj=project)\n    assert result.exit_code == 0\n\n\n@pytest.mark.parametrize(\n    \"python,platform\", [(\"==3.11\", \"macos\"), (\"==3.10\", \"manylinux_2_17_x86_64\"), (\"==3.11\", \"manylinux_2_17_aarch64\")]\n)\n@pytest.mark.parametrize(\"python_option\", [\"3.11\", \">=3.11\"])\ndef test_install_from_lock_with_incompatible_targets(project, pdm, python, platform, python_option):\n    pdm([\"lock\", \"--platform\", \"linux\", \"--python\", python_option], obj=project, strict=True)\n    project.environment.__dict__[\"spec\"] = EnvSpec.from_spec(python, platform)\n    result = pdm([\"install\"], obj=project)\n    assert result.exit_code == 1\n    assert \"No compatible lock target found\" in result.stderr\n\n\n@pytest.mark.network\n@pytest.mark.uv\ndef test_uv_install(project, pdm):\n    project.project_config.update({\"use_uv\": True, \"python.use_venv\": True})\n    project._saved_python = None\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    working_set = project.environment.get_working_set()\n    assert \"requests\" in working_set\n    assert \"urllib3\" in working_set\n    assert \"idna\" in working_set\n\n\n@pytest.mark.network\n@pytest.mark.uv\ndef test_uv_install_pep582_not_allowed(project, pdm):\n    project.project_config.update({\"use_uv\": True})\n    pdm([\"add\", \"requests\", \"--no-sync\"], obj=project, strict=True)\n    result = pdm([\"sync\"], obj=project)\n    assert result.exit_code == 1\n    assert \"doesn't support PEP 582 local packages\" in result.stderr\n"
  },
  {
    "path": "tests/cli/test_list.py",
    "content": "import json\nimport os\nimport pathlib\nfrom unittest import mock\n\nimport pytest\nfrom rich.box import ASCII\n\nfrom pdm.cli.commands.list import Command\nfrom pdm.models.specifiers import PySpecSet\nfrom pdm.pytest import Distribution\nfrom tests import FIXTURES\n\n\ndef test_list_command(project, pdm, mocker):\n    # Calls the correct handler within the Command\n    m = mocker.patch.object(Command, \"handle_list\")\n    pdm([\"list\"], obj=project)\n    m.assert_called_once()\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_list_graph_command(project, pdm, mocker):\n    # Calls the correct handler within the list command\n    m = mocker.patch.object(Command, \"handle_graph\")\n    pdm([\"list\", \"--tree\"], obj=project)\n    m.assert_called_once()\n\n\n@mock.patch(\"rich.console.ConsoleOptions.ascii_only\", lambda: True)\n@pytest.mark.usefixtures(\"working_set\")\ndef test_list_dependency_graph(project, pdm):\n    # Shows a line that contains a sub requirement (any order).\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    result = pdm([\"list\", \"--tree\"], obj=project)\n    expected = \"-- urllib3 1.22 [ required: <1.24,>=1.21.1 ]\" in result.outputs\n    assert expected, result.outputs\n\n\n@mock.patch(\"rich.console.ConsoleOptions.ascii_only\", lambda: True)\n@pytest.mark.usefixtures(\"working_set\")\ndef test_list_dependency_graph_include_exclude(project, pdm):\n    # Just include dev packages in the graph\n    project.environment.python_requires = PySpecSet(\">=3.6\")\n    dep_path = FIXTURES.joinpath(\"projects/demo\").as_posix()\n    pdm([\"add\", \"-de\", f\"{dep_path}[security]\"], obj=project, strict=True)\n\n    # Output full graph\n    result = pdm([\"list\", \"--tree\"], obj=project)\n    expects = (\n        \"demo 0.0.1 [ Not required ]\\n\",\n        \"+-- chardet 3.0.4 [ required: Any ]\\n\" if os.name == \"nt\" else \"\",\n        \"`-- idna 2.7 [ required: Any ]\\n\",\n        \"requests 2.19.1 [ Not required ]\\n\",\n        \"+-- certifi 2018.11.17 [ required: >=2017.4.17 ]\\n\",\n        \"+-- chardet 3.0.4 [ required: <3.1.0,>=3.0.2 ]\\n\",\n        \"+-- idna 2.7 [ required: <2.8,>=2.5 ]\\n\",\n        \"`-- urllib3 1.22 [ required: <1.24,>=1.21.1 ]\\n\",\n    )\n    expects = \"\".join(expects)\n    assert expects == result.outputs\n\n    # Now exclude the dev dep.\n    result = pdm([\"list\", \"--tree\", \"--exclude\", \"dev\"], obj=project)\n    expects = \"\"\n    assert expects == result.outputs\n\n    # Only include the dev dep\n    result = pdm([\"list\", \"--tree\", \"--include\", \"dev\", \"--exclude\", \"*\"], obj=project)\n    expects = \"demo[security] 0.0.1 [ required: Any ]\\n\"\n    expects = \"\".join(expects)\n    assert expects == result.outputs\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_list_dependency_graph_with_circular_forward(project, pdm, repository):\n    # shows a circular dependency\n    repository.add_candidate(\"foo\", \"0.1.0\")\n    repository.add_candidate(\"foo-bar\", \"0.1.0\")\n    repository.add_dependencies(\"foo\", \"0.1.0\", [\"foo-bar\"])\n    repository.add_dependencies(\"foo-bar\", \"0.1.0\", [\"foo\"])\n    pdm([\"add\", \"foo\"], obj=project, strict=True)\n    result = pdm([\"list\", \"--tree\"], obj=project)\n    circular_found = \"foo [circular]\" in result.outputs\n    assert circular_found\n\n\n@mock.patch(\"rich.console.ConsoleOptions.ascii_only\", lambda: True)\n@pytest.mark.usefixtures(\"working_set\")\ndef test_list_dependency_graph_with_circular_reverse(project, pdm, repository):\n    repository.add_candidate(\"foo\", \"0.1.0\")\n    repository.add_candidate(\"foo-bar\", \"0.1.0\")\n    repository.add_candidate(\"baz\", \"0.1.0\")\n    repository.add_dependencies(\"foo\", \"0.1.0\", [\"foo-bar\"])\n    repository.add_dependencies(\"foo-bar\", \"0.1.0\", [\"foo\", \"baz\"])\n    repository.add_dependencies(\"baz\", \"0.1.0\", [])\n    pdm([\"add\", \"foo\"], obj=project, strict=True)\n\n    # --reverse flag shows packages reversed and with [circular]\n    result = pdm([\"list\", \"--tree\", \"--reverse\"], obj=project)\n    expected = (\n        \"baz 0.1.0 \\n\"\n        \"`-- foo-bar 0.1.0 [ requires: Any ]\\n\"\n        \"    `-- foo 0.1.0 [ requires: Any ]\\n\"\n        \"        +-- foo-bar [circular] [ requires: Any ]\\n\"\n        \"        `-- test-project 0.0.0 [ requires: >=0.1.0 ]\\n\"\n    )\n    assert expected in result.outputs\n\n    # -r flag shows packages reversed and with [circular]\n    result = pdm([\"list\", \"--tree\", \"-r\"], obj=project)\n    assert expected in result.outputs\n\n\ndef test_list_reverse_without_graph_flag(project, pdm):\n    # results in PDMUsageError since --reverse needs --tree\n    result = pdm([\"list\", \"--reverse\"], obj=project)\n    assert \"[PdmUsageError]\" in result.stderr\n    assert \"--reverse cannot be used without --tree\" in result.stderr\n\n    result = pdm([\"list\", \"-r\"], obj=project)\n    assert \"[PdmUsageError]\" in result.stderr\n    assert \"--reverse cannot be used without --tree\" in result.stderr\n\n\n@mock.patch(\"rich.console.ConsoleOptions.ascii_only\", lambda: True)\n@pytest.mark.usefixtures(\"working_set\")\ndef test_list_reverse_dependency_graph(project, pdm):\n    # requests visible on leaf node\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    result = pdm([\"list\", \"--tree\", \"--reverse\"], obj=project)\n    assert \"`-- requests 2.19.1 [ requires: <1.24,>=1.21.1 ]\" in result.outputs\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_list_json(project, pdm):\n    # check json output matches graph output\n    pdm([\"add\", \"requests\", \"--no-self\"], obj=project, strict=True)\n    result = pdm([\"list\", \"--tree\", \"--json\"], obj=project)\n\n    expected = [\n        {\n            \"package\": \"requests\",\n            \"version\": \"2.19.1\",\n            \"required\": \">=2.19.1\",\n            \"dependencies\": [\n                {\n                    \"package\": \"certifi\",\n                    \"version\": \"2018.11.17\",\n                    \"required\": \">=2017.4.17\",\n                    \"dependencies\": [],\n                },\n                {\n                    \"package\": \"chardet\",\n                    \"version\": \"3.0.4\",\n                    \"required\": \"<3.1.0,>=3.0.2\",\n                    \"dependencies\": [],\n                },\n                {\n                    \"package\": \"idna\",\n                    \"version\": \"2.7\",\n                    \"required\": \"<2.8,>=2.5\",\n                    \"dependencies\": [],\n                },\n                {\n                    \"package\": \"urllib3\",\n                    \"version\": \"1.22\",\n                    \"required\": \"<1.24,>=1.21.1\",\n                    \"dependencies\": [],\n                },\n            ],\n        }\n    ]\n    assert expected == json.loads(result.outputs)\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_list_json_with_pattern(project, pdm):\n    pdm([\"add\", \"requests\", \"--no-self\"], obj=project, strict=True)\n    result = pdm([\"list\", \"--tree\", \"--json\", \"chardet\"], obj=project)\n\n    expected = [\n        {\n            \"package\": \"chardet\",\n            \"version\": \"3.0.4\",\n            \"required\": \"<3.1.0,>=3.0.2\",\n            \"dependencies\": [],\n        },\n    ]\n    assert expected == json.loads(result.outputs)\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_list_json_reverse(project, pdm):\n    # check json output matches reversed graph\n    pdm([\"add\", \"requests\", \"--no-self\"], obj=project, strict=True)\n    result = pdm([\"list\", \"--tree\", \"--reverse\", \"--json\"], obj=project)\n    expected = [\n        {\n            \"package\": \"certifi\",\n            \"version\": \"2018.11.17\",\n            \"requires\": None,\n            \"dependents\": [\n                {\n                    \"package\": \"requests\",\n                    \"version\": \"2.19.1\",\n                    \"requires\": \">=2017.4.17\",\n                    \"dependents\": [],\n                }\n            ],\n        },\n        {\n            \"package\": \"chardet\",\n            \"version\": \"3.0.4\",\n            \"requires\": None,\n            \"dependents\": [\n                {\n                    \"package\": \"requests\",\n                    \"version\": \"2.19.1\",\n                    \"requires\": \"<3.1.0,>=3.0.2\",\n                    \"dependents\": [],\n                }\n            ],\n        },\n        {\n            \"package\": \"idna\",\n            \"version\": \"2.7\",\n            \"requires\": None,\n            \"dependents\": [\n                {\n                    \"package\": \"requests\",\n                    \"version\": \"2.19.1\",\n                    \"requires\": \"<2.8,>=2.5\",\n                    \"dependents\": [],\n                }\n            ],\n        },\n        {\n            \"package\": \"urllib3\",\n            \"version\": \"1.22\",\n            \"requires\": None,\n            \"dependents\": [\n                {\n                    \"package\": \"requests\",\n                    \"version\": \"2.19.1\",\n                    \"requires\": \"<1.24,>=1.21.1\",\n                    \"dependents\": [],\n                }\n            ],\n        },\n    ]\n\n    assert expected == json.loads(result.outputs)\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_list_reverse_json_with_pattern(project, pdm):\n    # check json output matches reversed graph\n    pdm([\"add\", \"requests\", \"--no-self\"], obj=project, strict=True)\n    result = pdm([\"list\", \"--tree\", \"--reverse\", \"--json\", \"certifi\"], obj=project)\n    expected = [\n        {\n            \"package\": \"certifi\",\n            \"version\": \"2018.11.17\",\n            \"requires\": None,\n            \"dependents\": [\n                {\n                    \"package\": \"requests\",\n                    \"version\": \"2.19.1\",\n                    \"requires\": \">=2017.4.17\",\n                    \"dependents\": [],\n                }\n            ],\n        },\n    ]\n\n    assert expected == json.loads(result.outputs)\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_list_json_with_circular_forward(project, pdm, repository):\n    # circulars are handled in json exports\n    repository.add_candidate(\"foo\", \"0.1.0\")\n    repository.add_candidate(\"foo-bar\", \"0.1.0\")\n    repository.add_candidate(\"baz\", \"0.1.0\")\n    repository.add_dependencies(\"baz\", \"0.1.0\", [\"foo\"])\n    repository.add_dependencies(\"foo\", \"0.1.0\", [\"foo-bar\"])\n    repository.add_dependencies(\"foo-bar\", \"0.1.0\", [\"foo\"])\n    pdm([\"add\", \"baz\", \"--no-self\"], obj=project, strict=True)\n    result = pdm([\"list\", \"--tree\", \"--json\"], obj=project)\n    expected = [\n        {\n            \"package\": \"baz\",\n            \"version\": \"0.1.0\",\n            \"required\": \">=0.1.0\",\n            \"dependencies\": [\n                {\n                    \"package\": \"foo\",\n                    \"version\": \"0.1.0\",\n                    \"required\": \"Any\",\n                    \"dependencies\": [\n                        {\n                            \"package\": \"foo-bar\",\n                            \"version\": \"0.1.0\",\n                            \"required\": \"Any\",\n                            \"dependencies\": [\n                                {\n                                    \"package\": \"foo\",\n                                    \"version\": \"0.1.0\",\n                                    \"required\": \"Any\",\n                                    \"dependencies\": [],\n                                }\n                            ],\n                        }\n                    ],\n                }\n            ],\n        },\n    ]\n    assert expected == json.loads(result.outputs)\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_list_json_with_circular_reverse(project, pdm, repository):\n    # circulars are handled in reversed json exports\n    repository.add_candidate(\"foo\", \"0.1.0\")\n    repository.add_candidate(\"foo-bar\", \"0.1.0\")\n    repository.add_candidate(\"baz\", \"0.1.0\")\n    repository.add_dependencies(\"foo\", \"0.1.0\", [\"foo-bar\"])\n    repository.add_dependencies(\"foo-bar\", \"0.1.0\", [\"foo\", \"baz\"])\n    repository.add_dependencies(\"baz\", \"0.1.0\", [])\n    pdm([\"add\", \"foo\", \"--no-self\"], obj=project, strict=True)\n    result = pdm([\"list\", \"--tree\", \"--reverse\", \"--json\"], obj=project)\n    expected = [\n        {\n            \"package\": \"baz\",\n            \"version\": \"0.1.0\",\n            \"requires\": None,\n            \"dependents\": [\n                {\n                    \"package\": \"foo-bar\",\n                    \"version\": \"0.1.0\",\n                    \"requires\": \"Any\",\n                    \"dependents\": [\n                        {\n                            \"package\": \"foo\",\n                            \"version\": \"0.1.0\",\n                            \"requires\": \"Any\",\n                            \"dependents\": [\n                                {\n                                    \"package\": \"foo-bar\",\n                                    \"version\": \"0.1.0\",\n                                    \"requires\": \"Any\",\n                                    \"dependents\": [],\n                                }\n                            ],\n                        }\n                    ],\n                }\n            ],\n        },\n    ]\n    assert expected == json.loads(result.outputs)\n\n\ndef test_list_field_unknown(project, pdm):\n    # unknown list fields flagged to user\n    result = pdm([\"list\", \"--fields\", \"notvalid\"], obj=project)\n    assert \"[PdmUsageError]\" in result.stderr\n    assert \"--fields must specify one or more of:\" in result.stderr\n\n\ndef test_list_sort_unknown(project, pdm):\n    # unknown sort fields flagged to user\n    result = pdm([\"list\", \"--sort\", \"notvalid\"], obj=project)\n    assert \"[PdmUsageError]\" in result.stderr\n    assert \"--sort key must be one of:\" in result.stderr\n\n\ndef test_list_freeze_banned_options(project, pdm):\n    # other flags cannot be used with --freeze\n    result = pdm([\"list\", \"--freeze\", \"--tree\"], obj=project)\n    expected = \"--tree cannot be used with --freeze\"\n    assert expected in result.outputs\n\n    result = pdm([\"list\", \"--freeze\", \"--reverse\"], obj=project)\n    expected = \"--reverse cannot be used without --tree\"\n    assert expected in result.outputs\n\n    result = pdm([\"list\", \"--freeze\", \"-r\"], obj=project)\n    expected = \"--reverse cannot be used without --tree\"\n    assert expected in result.outputs\n\n    result = pdm([\"list\", \"--freeze\", \"--fields\", \"name\"], obj=project)\n    expected = \"--fields cannot be used with --freeze\"\n    assert expected in result.outputs\n\n    result = pdm([\"list\", \"--freeze\", \"--resolve\"], obj=project)\n    expected = \"--resolve cannot be used with --freeze\"\n    assert expected in result.outputs\n\n    result = pdm([\"list\", \"--freeze\", \"--sort\", \"version\"], obj=project)\n    expected = \"--sort cannot be used with --freeze\"\n    assert expected in result.outputs\n\n    result = pdm([\"list\", \"--freeze\", \"--csv\"], obj=project)\n    expected = \"--csv cannot be used with --freeze\"\n    assert expected in result.outputs\n\n    result = pdm([\"list\", \"--freeze\", \"--json\"], obj=project)\n    expected = \"--json cannot be used with --freeze\"\n    assert expected in result.outputs\n\n    result = pdm([\"list\", \"--freeze\", \"--markdown\"], obj=project)\n    expected = \"--markdown cannot be used with --freeze\"\n    assert expected in result.outputs\n\n    result = pdm([\"list\", \"--freeze\", \"--include\", \"dev\"], obj=project)\n    expected = \"--include/--exclude cannot be used with --freeze\"\n    assert expected in result.outputs\n\n    result = pdm([\"list\", \"--freeze\", \"--exclude\", \"dev\"], obj=project)\n    expected = \"--include/--exclude cannot be used with --freeze\"\n    assert expected in result.outputs\n\n\ndef test_list_multiple_export_formats(project, pdm):\n    # export formats cannot be used with each other\n    result = pdm([\"list\", \"--csv\", \"--markdown\"], obj=project)\n    expected = \"--markdown: not allowed with argument --csv\"\n    assert expected in result.outputs\n\n    result = pdm([\"list\", \"--csv\", \"--json\"], obj=project)\n    expected = \"--json: not allowed with argument --csv\"\n    assert expected in result.outputs\n\n    result = pdm([\"list\", \"--markdown\", \"--csv\"], obj=project)\n    expected = \"--csv: not allowed with argument --markdown\"\n    assert expected in result.outputs\n\n    result = pdm([\"list\", \"--markdown\", \"--json\"], obj=project)\n    expected = \"--json: not allowed with argument --markdown\"\n    assert expected in result.outputs\n\n    result = pdm([\"list\", \"--json\", \"--markdown\"], obj=project)\n    expected = \"--markdown: not allowed with argument --json\"\n    assert expected in result.outputs\n\n    result = pdm([\"list\", \"--json\", \"--csv\"], obj=project)\n    expected = \"--csv: not allowed with argument --json\"\n    assert expected in result.outputs\n\n\n@mock.patch(\"pdm.termui.ROUNDED\", ASCII)\n@pytest.mark.usefixtures(\"working_set\")\ndef test_list_bare(project, pdm):\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    result = pdm([\"list\"], obj=project)\n    # Ordering can be different on different platforms\n    # and python versions.\n    assert \"| name         | version    | location |\\n\" in result.output\n    assert \"| certifi      | 2018.11.17 |          |\\n\" in result.output\n    assert \"| chardet      | 3.0.4      |          |\\n\" in result.output\n    assert \"| idna         | 2.7        |          |\\n\" in result.output\n    assert \"| requests     | 2.19.1     |          |\\n\" in result.output\n    assert \"| urllib3      | 1.22       |          |\\n\" in result.output\n    assert \"| test-project | 0.0.0      |          |\\n\" in result.output\n\n\n@mock.patch(\"pdm.termui.ROUNDED\", ASCII)\n@pytest.mark.usefixtures(\"working_set\")\ndef test_list_bare_sorted_name(project, pdm):\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    result = pdm([\"list\", \"--sort\", \"name\"], obj=project)\n    expected = (\n        \"+--------------------------------------+\\n\"\n        \"| name         | version    | location |\\n\"\n        \"|--------------+------------+----------|\\n\"\n        \"| certifi      | 2018.11.17 |          |\\n\"\n        \"| chardet      | 3.0.4      |          |\\n\"\n        \"| idna         | 2.7        |          |\\n\"\n        \"| requests     | 2.19.1     |          |\\n\"\n        \"| test-project | 0.0.0      |          |\\n\"\n        \"| urllib3      | 1.22       |          |\\n\"\n        \"+--------------------------------------+\\n\"\n    )\n    assert expected == result.output\n\n\n@mock.patch(\"pdm.termui.ROUNDED\", ASCII)\n@pytest.mark.usefixtures(\"working_set\")\ndef test_list_with_pattern(project, pdm):\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    result = pdm([\"list\", \"--sort\", \"name\", \"c*\"], obj=project)\n    expected = (\n        \"+---------------------------------+\\n\"\n        \"| name    | version    | location |\\n\"\n        \"|---------+------------+----------|\\n\"\n        \"| certifi | 2018.11.17 |          |\\n\"\n        \"| chardet | 3.0.4      |          |\\n\"\n        \"+---------------------------------+\\n\"\n    )\n    assert expected == result.output\n\n\n@pytest.fixture()\ndef fake_working_set(working_set):\n    \"\"\"Create fake packages with license data\n    for testing.\n    \"\"\"\n\n    class _MockPackagePath(pathlib.PurePosixPath):\n        def read_text(self, *args, **kwargs):\n            return self.license_text\n\n    # Foo package (adapted to contain newlines in License field)\n    # e.g. via license = { file=\"LICENSE\" }\n    foo = Distribution(\n        \"foo\",\n        \"0.1.0\",\n        metadata={\n            \"License\": \"A License\\n\\nextra\\ntext\",\n            \"Classifier\": \"License :: A License\",\n        },\n    )\n    foo_l = _MockPackagePath(\"foo-0.1.0.dist-info\", \"LICENSE\")\n    foo_l.license_text = \"license text for foo here\"\n    foo.files = [foo_l]\n\n    # Bar package.\n    bar = Distribution(\"bar\", \"3.0.1\", metadata={\"License\": \"B License\"})\n    bar_l = _MockPackagePath(\"bar-3.0.1.dist-info\", \"LICENSE\")\n    bar_l.license_text = \"license text for bar here\"\n    bar.files = [bar_l]\n\n    # Baz package.\n    baz = Distribution(\"baz\", \"2.7\", metadata={\"License\": \"C License\"})\n    baz_l = _MockPackagePath(\"bar-2.7.dist-info\", \"LICENSE\")\n    baz_l.license_text = \"license text for baz here\"\n    baz.files = [baz_l]\n\n    # missing package- License is set to UNKNOWN, text saved in COPYING\n    unknown = Distribution(\n        \"unknown\",\n        \"1.0\",\n        metadata={\n            \"License\": \"UNKNOWN\",\n            \"Classifier\": \"License :: OSI Approved :: Apache Software License\",\n        },\n    )\n    unknown_l = _MockPackagePath(\"unknown-1.0.dist-info\", \"COPYING\")\n    unknown_l.license_text = \"license text for UNKNOWN here\"\n    unknown.files = [unknown_l]\n\n    # missing package- License is set to UNKNOWN, text saved in LICENCE\n    # using UK spelling\n    classifier = Distribution(\"classifier\", \"1.0\", metadata={\"Classifier\": \"License :: PDM TEST D\"})\n    classifier_l = _MockPackagePath(\"classifier-1.0.dist-info\", \"LICENCE\")\n    classifier_l.license_text = \"license text for CLASSIFIER here\"\n    classifier_l.read_text = lambda *a, **kw: 1 / 0  # make it throw an error\n    classifier.files = [classifier_l]\n\n    # Place our fake packages in the working set.\n    for candidate in [foo, bar, baz, unknown, classifier]:\n        working_set.add_distribution(candidate)\n    return working_set\n\n\n@pytest.fixture()\ndef fake_metadata(mocker, repository):\n    def prepare_metadata(self):\n        can = self.candidate\n        version, dependencies = repository.get_raw_dependencies(can)\n        dist = Distribution(can.name, version, can.req.editable)\n        dist.dependencies = dependencies\n        return dist\n\n    return mocker.patch(\n        \"pdm.models.candidates.PreparedCandidate.prepare_metadata\",\n        prepare_metadata,\n    )\n\n\n@mock.patch(\"pdm.termui.ROUNDED\", ASCII)\n@pytest.mark.usefixtures(\"working_set\")\ndef test_list_freeze(project, pdm):\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    result = pdm([\"list\", \"--freeze\"], obj=project)\n    expected = \"certifi==2018.11.17\\nchardet==3.0.4\\nidna==2.7\\nrequests==2.19.1\\ntest-project==0.0.0\\nurllib3==1.22\\n\"\n    assert expected == result.output\n\n\n@mock.patch(\"pdm.termui.ROUNDED\", ASCII)\n@pytest.mark.usefixtures(\"working_set\")\ndef test_list_bare_sorted_version(project, pdm):\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    result = pdm([\"list\", \"--sort\", \"version\"], obj=project)\n    expected = (\n        \"+--------------------------------------+\\n\"\n        \"| name         | version    | location |\\n\"\n        \"|--------------+------------+----------|\\n\"\n        \"| test-project | 0.0.0      |          |\\n\"\n        \"| urllib3      | 1.22       |          |\\n\"\n        \"| requests     | 2.19.1     |          |\\n\"\n        \"| idna         | 2.7        |          |\\n\"\n        \"| certifi      | 2018.11.17 |          |\\n\"\n        \"| chardet      | 3.0.4      |          |\\n\"\n        \"+--------------------------------------+\\n\"\n    )\n    assert expected == result.output\n\n\n@mock.patch(\"pdm.termui.ROUNDED\", ASCII)\n@pytest.mark.usefixtures(\"fake_metadata\")\ndef test_list_bare_sorted_version_resolve(project, pdm, working_set):\n    project.environment.python_requires = PySpecSet(\">=3.6\")\n    pdm([\"add\", \"requests\", \"--no-sync\"], obj=project, strict=True)\n\n    result = pdm([\"list\", \"--sort\", \"version\", \"--resolve\"], obj=project, strict=True)\n    assert \"requests\" not in working_set\n    expected = (\n        \"+----------------------------------+\\n\"\n        \"| name     | version    | location |\\n\"\n        \"|----------+------------+----------|\\n\"\n        \"| urllib3  | 1.22       |          |\\n\"\n        \"| requests | 2.19.1     |          |\\n\"\n        \"| idna     | 2.7        |          |\\n\"\n        \"| certifi  | 2018.11.17 |          |\\n\"\n        \"| chardet  | 3.0.4      |          |\\n\"\n        \"+----------------------------------+\\n\"\n    )\n    assert expected == result.outputs, result.outputs\n\n\n@mock.patch(\"pdm.termui.ROUNDED\", ASCII)\n@pytest.mark.usefixtures(\"fake_working_set\")\ndef test_list_bare_fields_licences(project, pdm):\n    result = pdm([\"list\", \"--fields\", \"name,version,groups,licenses\"], obj=project)\n    expected = (\n        \"+---------------------------------------------------------+\\n\"\n        \"| name       | version | groups | licenses                |\\n\"\n        \"|------------+---------+--------+-------------------------|\\n\"\n        \"| bar        | 3.0.1   | :sub   | B License               |\\n\"\n        \"| baz        | 2.7     | :sub   | C License               |\\n\"\n        \"| classifier | 1.0     | :sub   | PDM TEST D              |\\n\"\n        \"| foo        | 0.1.0   | :sub   | A License               |\\n\"\n        \"| unknown    | 1.0     | :sub   | Apache Software License |\\n\"\n        \"+---------------------------------------------------------+\\n\"\n    )\n    assert expected == result.output\n\n\n@pytest.mark.usefixtures(\"fake_working_set\")\ndef test_list_csv_fields_licences(project, pdm):\n    result = pdm([\"list\", \"--csv\", \"--fields\", \"name,version,licenses\"], obj=project)\n    expected = (\n        \"name,version,licenses\\n\"\n        \"bar,3.0.1,B License\\n\"\n        \"baz,2.7,C License\\n\"\n        \"classifier,1.0,PDM TEST D\\n\"\n        \"foo,0.1.0,A License\\n\"\n        \"unknown,1.0,Apache Software License\\n\"\n    )\n    assert expected == result.output\n\n\n@pytest.mark.usefixtures(\"fake_working_set\")\ndef test_list_json_fields_licences(project, pdm):\n    result = pdm([\"list\", \"--json\", \"--fields\", \"name,version,licenses\"], obj=project)\n    expected = [\n        {\"name\": \"bar\", \"version\": \"3.0.1\", \"licenses\": \"B License\"},\n        {\"name\": \"baz\", \"version\": \"2.7\", \"licenses\": \"C License\"},\n        {\"name\": \"classifier\", \"version\": \"1.0\", \"licenses\": \"PDM TEST D\"},\n        {\"name\": \"foo\", \"version\": \"0.1.0\", \"licenses\": \"A License\"},\n        {\"name\": \"unknown\", \"version\": \"1.0\", \"licenses\": \"Apache Software License\"},\n    ]\n\n    assert expected == json.loads(result.outputs)\n\n\n@pytest.mark.usefixtures(\"fake_working_set\")\ndef test_list_markdown_fields_licences(project, pdm):\n    # Note that in \"foo\" the \"License\" metadata field (\"License\": \"A License\\n\\nextra\\ntext\")\n    # is ignored, in favour of the classifier and the LICENSE file.\n    # This behaviour could be improved.\n    result = pdm([\"list\", \"--markdown\", \"--fields\", \"name,version,licenses\"], obj=project)\n    expected = (\n        \"# test-project licenses\\n\"\n        \"## bar\\n\\n\"\n        \"| Name | bar |\\n\"\n        \"|----|----|\\n\"\n        \"| Version | 3.0.1 |\\n\"\n        \"| Licenses | B License |\\n\\n\"\n        \"bar-3.0.1.dist-info/LICENSE\\n\\n\\n\"\n        \"````\\n\"\n        \"license text for bar here\\n\"\n        \"````\\n\\n\\n\"\n        \"## baz\\n\\n\"\n        \"| Name | baz |\\n\"\n        \"|----|----|\\n\"\n        \"| Version | 2.7 |\\n\"\n        \"| Licenses | C License |\\n\\n\"\n        \"bar-2.7.dist-info/LICENSE\\n\\n\\n\"\n        \"````\\n\"\n        \"license text for baz here\\n\"\n        \"````\\n\\n\\n\"\n        \"## classifier\\n\\n\"\n        \"| Name | classifier |\\n\"\n        \"|----|----|\\n\"\n        \"| Version | 1.0 |\\n\"\n        \"| Licenses | PDM TEST D |\\n\\n\"\n        \"classifier-1.0.dist-info/LICENCE\\n\\n\\n\"\n        \"````\\n\"\n        \"Problem finding license text: division by zero\\n\"\n        \"````\\n\\n\\n\"\n        \"## foo\\n\\n\"\n        \"| Name | foo |\\n\"\n        \"|----|----|\\n\"\n        \"| Version | 0.1.0 |\\n\"\n        \"| Licenses | A License |\\n\\n\"\n        \"foo-0.1.0.dist-info/LICENSE\\n\\n\\n\"\n        \"````\\n\"\n        \"license text for foo here\\n\"\n        \"````\\n\\n\\n\"\n        \"## unknown\\n\\n\"\n        \"| Name | unknown |\\n\"\n        \"|----|----|\\n\"\n        \"| Version | 1.0 |\\n\"\n        \"| Licenses | Apache Software License |\\n\\n\"\n        \"unknown-1.0.dist-info/COPYING\\n\\n\\n\"\n        \"````\\n\"\n        \"license text for UNKNOWN here\\n\"\n        \"````\\n\\n\\n\"\n    )\n    assert expected == result.output\n\n\n@pytest.mark.usefixtures(\"working_set\", \"repository\")\ndef test_list_csv_include_exclude_valid(project, pdm):\n    project.environment.python_requires = PySpecSet(\">=3.6\")\n    dep_path = FIXTURES.joinpath(\"projects/demo\").as_posix()\n    pdm([\"add\", \"-de\", f\"{dep_path}[security]\"], obj=project, strict=True)\n    result = pdm(\n        [\n            \"list\",\n            \"--csv\",\n            \"--fields\",\n            \"name,version,groups\",\n            \"--sort\",\n            \"name\",\n            \"--include\",\n            \"notexisting\",\n        ],\n        obj=project,\n    )\n    assert \"[PdmUsageError]\" in result.outputs\n    assert \"--include groups must be selected from\" in result.outputs\n    assert \"dev\" in result.outputs\n    assert \"default\" in result.outputs\n    assert \":sub\" in result.outputs\n\n\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_list_packages_in_given_venv(project, pdm):\n    project.pyproject.metadata[\"requires-python\"] = \">=3.7\"\n    project.pyproject.write()\n    project.global_config[\"python.use_venv\"] = True\n    pdm([\"venv\", \"create\"], obj=project, strict=True)\n    pdm([\"venv\", \"create\", \"--name\", \"second\"], obj=project, strict=True)\n    project._saved_python = None\n    pdm([\"add\", \"first\", \"--no-self\"], obj=project, strict=True)\n    second_lockfile = str(project.root / \"pdm.2.lock\")\n    pdm(\n        [\"add\", \"-G\", \"second\", \"--no-self\", \"-L\", second_lockfile, \"--venv\", \"second\", \"editables\"],\n        obj=project,\n        strict=True,\n    )\n    project.environment = None\n    result1 = pdm([\"list\", \"--freeze\"], obj=project, strict=True)\n    result2 = pdm([\"list\", \"--freeze\", \"--venv\", \"second\"], obj=project, strict=True)\n    assert result1.output.strip() == \"first==2.0.2\"\n    assert result2.output.strip() == \"editables==0.2\"\n\n\n@pytest.mark.usefixtures(\"working_set\", \"repository\")\ndef test_list_csv_include_exclude(project, pdm):\n    project.environment.python_requires = PySpecSet(\">=3.6\")\n    dep_path = FIXTURES.joinpath(\"projects/demo\").as_posix()\n    pdm([\"add\", \"-de\", f\"{dep_path}[security]\"], obj=project, strict=True)\n\n    # Show all groups.\n    result = pdm(\n        [\"list\", \"--csv\", \"--fields\", \"name,version,groups\", \"--sort\", \"name\"],\n        obj=project,\n    )\n    expected = (\n        \"name,version,groups\\n\"\n        \"certifi,2018.11.17,:sub\\n\"\n        \"chardet,3.0.4,:sub\\n\"\n        \"demo,0.0.1,dev\\n\"\n        \"idna,2.7,:sub\\n\"\n        \"requests,2.19.1,:sub\\n\"\n        \"urllib3,1.22,:sub\\n\"\n    )\n    assert expected == result.output\n\n    # Sub always included.\n    result = pdm(\n        [\n            \"list\",\n            \"--csv\",\n            \"--fields\",\n            \"name,groups\",\n            \"--sort\",\n            \"name\",\n            \"--include\",\n            \"dev\",\n        ],\n        obj=project,\n    )\n    expected = \"name,groups\\ncertifi,:sub\\nchardet,:sub\\ndemo,dev\\nidna,:sub\\nrequests,:sub\\nurllib3,:sub\\n\"\n    assert expected == result.output\n\n    # Include all (default) except sub\n    result = pdm(\n        [\n            \"list\",\n            \"--csv\",\n            \"--fields\",\n            \"name,groups\",\n            \"--sort\",\n            \"name\",\n            \"--exclude\",\n            \":sub\",\n        ],\n        obj=project,\n    )\n    expected = \"name,groups\\ndemo,dev\\n\"\n    assert expected == result.output\n\n    # Show just the dev group\n    result = pdm(\n        [\n            \"list\",\n            \"--csv\",\n            \"--fields\",\n            \"name,version,groups\",\n            \"--sort\",\n            \"name\",\n            \"--include\",\n            \"dev\",\n            \"--exclude\",\n            \"*\",\n        ],\n        obj=project,\n    )\n    expected = \"name,version,groups\\ndemo,0.0.1,dev\\n\"\n    assert expected == result.output\n\n    # Exclude the dev group.\n    result = pdm(\n        [\n            \"list\",\n            \"--csv\",\n            \"--fields\",\n            \"name,version,groups\",\n            \"--sort\",\n            \"name\",\n            \"--exclude\",\n            \"dev\",\n        ],\n        obj=project,\n    )\n    expected = \"name,version,groups\\n\"\n    assert expected == result.output\n"
  },
  {
    "path": "tests/cli/test_lock.py",
    "content": "from pathlib import Path\nfrom unittest.mock import ANY\n\nimport pytest\nfrom unearth import Link\n\nfrom pdm.cli import actions\nfrom pdm.exceptions import PdmUsageError\nfrom pdm.models.requirements import parse_requirement\nfrom pdm.models.specifiers import PySpecSet\nfrom pdm.project.lockfile import FLAG_CROSS_PLATFORM\nfrom pdm.project.lockfile.base import Compatibility\nfrom pdm.utils import parse_version\nfrom tests import FIXTURES\n\n\ndef test_lock_command(project, pdm, mocker):\n    m = mocker.patch.object(actions, \"do_lock\")\n    pdm([\"lock\"], obj=project)\n    m.assert_called_with(\n        project,\n        refresh=False,\n        groups=[\"default\"],\n        hooks=ANY,\n        strategy_change=None,\n        strategy=\"all\",\n        append=False,\n        env_spec=None,\n    )\n\n\n@pytest.mark.usefixtures(\"repository\")\n@pytest.mark.parametrize(\"lock_format\", [\"pdm\", \"pylock\"])\ndef test_lock_dependencies(project, lock_format):\n    project.add_dependencies([\"requests\"])\n    project.project_config[\"lock.format\"] = lock_format\n    actions.do_lock(project)\n    assert project.lockfile.exists()\n    assert project.lockfile._path.name == \"pdm.lock\" if lock_format == \"pdm\" else \"pylock.toml\"\n    locked = project.get_locked_repository().candidates\n    for package in (\"requests\", \"idna\", \"chardet\", \"certifi\"):\n        assert package in locked\n\n\n@pytest.mark.parametrize(\"args\", [(\"-S\", \"static_urls\"), (\"--static-urls\",)])\ndef test_lock_refresh(pdm, project, repository, args, core, mocker):\n    project.add_dependencies([\"requests\"])\n    result = pdm([\"lock\"], obj=project)\n    assert result.exit_code == 0\n    assert project.is_lockfile_hash_match()\n    package = next(p for p in project.lockfile[\"package\"] if p[\"name\"] == \"requests\")\n    assert not package.get(\"files\")\n    project.add_dependencies([\"requests>=2.0\"])\n    url_hashes = {\n        \"http://example.com/requests-2.19.1-py3-none-any.whl\": \"sha256:abcdef123456\",\n        \"http://example2.com/requests-2.19.1-py3-none-AMD64.whl\": \"sha256:abcdef123456\",\n        \"http://example1.com/requests-2.19.1-py3-none-any.whl\": \"sha256:abcdef123456\",\n    }\n    mocker.patch.object(\n        core.repository_class,\n        \"get_hashes\",\n        side_effect=(\n            lambda c: (\n                [{\"url\": url, \"file\": Link(url).filename, \"hash\": hash} for url, hash in url_hashes.items()]\n                if c.identify() == \"requests\"\n                else []\n            )\n        ),\n    )\n    assert not project.is_lockfile_hash_match()\n    result = pdm([\"lock\", \"--refresh\", \"-v\"], obj=project)\n    assert result.exit_code == 0\n    package = next(p for p in project.lockfile[\"package\"] if p[\"name\"] == \"requests\")\n    assert package[\"files\"] == [\n        {\"file\": \"requests-2.19.1-py3-none-AMD64.whl\", \"hash\": \"sha256:abcdef123456\"},\n        {\"file\": \"requests-2.19.1-py3-none-any.whl\", \"hash\": \"sha256:abcdef123456\"},\n    ]\n    assert project.is_lockfile_hash_match()\n    result = pdm([\"lock\", \"--refresh\", *args, \"-v\"], obj=project)\n    assert result.exit_code == 0\n    package = next(p for p in project.lockfile[\"package\"] if p[\"name\"] == \"requests\")\n    assert package[\"files\"] == [{\"url\": url, \"hash\": hash} for url, hash in sorted(url_hashes.items())]\n\n\ndef test_lock_refresh_keep_consistent(pdm, project, repository):\n    project.add_dependencies([\"requests\"])\n    result = pdm([\"lock\"], obj=project)\n    assert result.exit_code == 0\n    assert project.is_lockfile_hash_match()\n    previous = project.lockfile._path.read_text()\n    result = pdm([\"lock\", \"--refresh\"], obj=project)\n    assert result.exit_code == 0\n    assert project.lockfile._path.read_text() == previous\n\n\ndef test_lock_check_no_change_success(pdm, project, repository):\n    project.add_dependencies([\"requests\"])\n    result = pdm([\"lock\"], obj=project)\n    assert result.exit_code == 0\n    assert project.is_lockfile_hash_match()\n\n    result = pdm([\"lock\", \"--check\"], obj=project)\n    assert result.exit_code == 0\n\n\ndef test_lock_check_change_fails(pdm, project, repository):\n    project.add_dependencies([\"requests\"])\n    result = pdm([\"lock\"], obj=project)\n    assert result.exit_code == 0\n    assert project.is_lockfile_hash_match()\n\n    project.add_dependencies([\"pyyaml\"])\n    result = pdm([\"lock\", \"--check\"], obj=project)\n    assert result.exit_code == 1\n\n\n@pytest.mark.usefixtures(\"repository\")\ndef test_innovations_with_specified_lockfile(pdm, project, working_set):\n    project.add_dependencies([\"requests\"])\n    lockfile = str(project.root / \"mylock.lock\")\n    pdm([\"lock\", \"--lockfile\", lockfile], strict=True, obj=project)\n    assert project.lockfile._path == project.root / \"mylock.lock\"\n    assert project.is_lockfile_hash_match()\n    locked = project.get_locked_repository().candidates\n    assert \"requests\" in locked\n    pdm([\"sync\", \"--lockfile\", lockfile], strict=True, obj=project)\n    assert \"requests\" in working_set\n\n\n@pytest.mark.usefixtures(\"repository\", \"vcs\")\ndef test_skip_editable_dependencies_in_metadata(project, capsys):\n    project.pyproject.metadata[\"dependencies\"] = [\n        \"-e git+https://github.com/test-root/demo.git@1234567890abcdef#egg=demo\"\n    ]\n    actions.do_lock(project)\n    _, err = capsys.readouterr()\n    assert \"WARNING: Skipping editable dependency\" in err\n    assert not project.get_locked_repository().candidates\n\n\n@pytest.mark.usefixtures(\"repository\")\ndef test_lock_selected_groups(project, pdm):\n    project.add_dependencies([\"requests\"], to_group=\"http\")\n    project.add_dependencies([\"pytz\"])\n    pdm([\"lock\", \"-G\", \"http\", \"--no-default\"], obj=project, strict=True)\n    assert project.lockfile.groups == [\"http\"]\n    assert \"requests\" in project.get_locked_repository().candidates\n    assert \"pytz\" not in project.get_locked_repository().candidates\n\n\n@pytest.mark.usefixtures(\"repository\")\n@pytest.mark.parametrize(\"to_dev\", [True, False])\ndef test_lock_self_referencing_dev_groups(project, pdm, to_dev):\n    name = project.name\n    project.add_dependencies([\"requests\"], to_group=\"http\", dev=to_dev)\n    project.add_dependencies(\n        {\"pytz\": parse_requirement(\"pytz\"), f\"{name}[http]\": parse_requirement(f\"{name}[http]\")},\n        to_group=\"dev\",\n        dev=True,\n    )\n    pdm([\"lock\", \"-G\", \"dev\"], obj=project, strict=True)\n    assert project.lockfile.groups == [\"default\", \"dev\", \"http\"]\n    packages = project.lockfile[\"package\"]\n    pytz = next(p for p in packages if p[\"name\"] == \"pytz\")\n    assert pytz[\"groups\"] == [\"dev\"]\n    requests = next(p for p in packages if p[\"name\"] == \"requests\")\n    assert requests[\"groups\"] == [\"dev\", \"http\"]\n    idna = next(p for p in packages if p[\"name\"] == \"idna\")\n    assert idna[\"groups\"] == [\"dev\", \"http\"]\n\n\n@pytest.mark.usefixtures(\"repository\")\ndef test_lock_self_referencing_optional_groups(project, pdm):\n    name = project.name\n    project.add_dependencies([\"requests\"], to_group=\"http\")\n    project.add_dependencies(\n        {\"pytz\": parse_requirement(\"pytz\"), f\"{name}[http]\": parse_requirement(f\"{name}[http]\")},\n        to_group=\"all\",\n    )\n    pdm([\"lock\", \"-G\", \"all\"], obj=project, strict=True)\n    assert project.lockfile.groups == [\"default\", \"all\", \"http\"]\n    packages = project.lockfile[\"package\"]\n    pytz = next(p for p in packages if p[\"name\"] == \"pytz\")\n    assert pytz[\"groups\"] == [\"all\"]\n    requests = next(p for p in packages if p[\"name\"] == \"requests\")\n    assert requests[\"groups\"] == [\"all\", \"http\"]\n    idna = next(p for p in packages if p[\"name\"] == \"idna\")\n    assert idna[\"groups\"] == [\"all\", \"http\"]\n\n\n@pytest.mark.usefixtures(\"repository\")\ndef test_lock_include_groups_not_allowed(project, pdm):\n    project.pyproject.metadata[\"optional-dependencies\"] = {\"http\": [\"requests\"]}\n    project.pyproject.dependency_groups.update({\"dev\": [\"pytest\", {\"include-group\": \"http\"}]})\n    project.pyproject.write()\n    result = pdm([\"lock\", \"-G\", \"all\"], obj=project)\n    assert result.exit_code != 0\n    assert \"Missing group 'http' in `include-group`\" in result.stderr\n\n\n@pytest.mark.usefixtures(\"repository\")\ndef test_lock_optional_referencing_dev_group_not_allowed(project, pdm):\n    name = project.name\n    project.pyproject.metadata[\"optional-dependencies\"] = {\"http\": [\"requests\", f\"{name}[dev]\"]}\n    project.pyproject.dependency_groups.update({\"dev\": [\"pytest\"]})\n    project.pyproject.write()\n    result = pdm([\"lock\", \"-G\", \"http\"], obj=project)\n    assert result.exit_code != 0\n    assert \"Optional dependency group 'http' cannot include non-existing extras\" in result.stderr\n\n\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_lock_multiple_platform_wheels(project, pdm):\n    project.environment.python_requires = PySpecSet(\">=3.7\")\n    project.add_dependencies([\"pdm-hello\"])\n    pdm([\"lock\"], obj=project, strict=True)\n    package = next(p for p in project.lockfile[\"package\"] if p[\"name\"] == \"pdm-hello\")\n    file_hashes = package[\"files\"]\n    assert len(file_hashes) == 2\n\n\n@pytest.mark.usefixtures(\"local_finder\")\n@pytest.mark.parametrize(\"platform\", [\"linux\", \"macos\", \"windows\"])\ndef test_lock_specific_platform_wheels(project, pdm, platform):\n    project.environment.python_requires = PySpecSet(\">=3.7\")\n    project.add_dependencies([\"pdm-hello\"])\n    pdm([\"lock\", \"--platform\", platform], obj=project, strict=True)\n    assert FLAG_CROSS_PLATFORM not in project.lockfile.strategy\n    package = next(p for p in project.lockfile[\"package\"] if p[\"name\"] == \"pdm-hello\")\n    file_hashes = package[\"files\"]\n    wheels_num = 2 if platform == \"windows\" else 1\n    assert len(file_hashes) == wheels_num\n\n\ndef test_parse_lock_strategy_group_options(core):\n    core.init_parser()\n    parser = core.parser\n\n    ns = parser.parse_args([\"lock\", \"-S\", \"no_cross_platform\"])\n    assert ns.strategy_change == [\"no_cross_platform\"]\n    ns = parser.parse_args([\"lock\", \"-S\", \"no_cross_platform\", \"--static-urls\"])\n    assert ns.strategy_change == [\"no_cross_platform\", \"static_urls\"]\n    ns = parser.parse_args([\"lock\", \"-S\", \"no_cross_platform,direct_minimal_versions\"])\n    assert ns.strategy_change == [\"no_cross_platform\", \"direct_minimal_versions\"]\n\n\ndef test_apply_lock_strategy_changes(project):\n    assert project.lockfile.apply_strategy_change([\"no_cross_platform\", \"static_urls\"]) == {\n        \"inherit_metadata\",\n        \"static_urls\",\n    }\n    assert project.lockfile.apply_strategy_change([\"no_static_urls\"]) == {\"inherit_metadata\"}\n    assert project.lockfile.apply_strategy_change([\"no_inherit_metadata\"]) == set()\n\n\n@pytest.mark.parametrize(\"strategy\", [[\"abc\"], [\"no_abc\", \"static_urls\"]])\ndef test_apply_lock_strategy_changes_invalid(project, strategy):\n    with pytest.raises(PdmUsageError):\n        project.lockfile.apply_strategy_change(strategy)\n\n\ndef test_lock_direct_minimal_versions(project, repository, pdm):\n    project.add_dependencies([\"django\"])\n    repository.add_candidate(\"pytz\", \"2019.6\")\n    pdm([\"lock\", \"-S\", \"direct_minimal_versions\"], obj=project, strict=True)\n    assert project.lockfile.strategy == {\"direct_minimal_versions\", \"inherit_metadata\"}\n    locked_repository = project.get_locked_repository()\n    assert locked_repository.candidates[\"django\"].version == \"1.11.8\"\n    assert locked_repository.candidates[\"pytz\"].version == \"2019.6\"\n\n\n@pytest.mark.usefixtures(\"local_finder\")\n@pytest.mark.parametrize(\"args\", [(), (\"-S\", \"direct_minimal_versions\")])\ndef test_lock_direct_minimal_versions_real(project, pdm, args):\n    project.add_dependencies([\"zipp\"])\n    pdm([\"lock\", *args], obj=project, strict=True)\n    locked_candidate = project.get_locked_repository().candidates[\"zipp\"]\n    if args:\n        assert locked_candidate.version == \"3.6.0\"\n    else:\n        assert locked_candidate.version == \"3.7.0\"\n\n\n@pytest.mark.parametrize(\n    \"lock_version,expected\",\n    [\n        (\"4.1.0\", Compatibility.BACKWARD),\n        (\"4.1.1\", Compatibility.SAME),\n        (\"4.1.2\", Compatibility.FORWARD),\n        (\"4.2\", Compatibility.NONE),\n        (\"3.0\", Compatibility.NONE),\n        (\"4.0.1\", Compatibility.BACKWARD),\n    ],\n)\ndef test_lockfile_compatibility(project, monkeypatch, lock_version, expected, pdm):\n    pdm([\"lock\"], obj=project, strict=True)\n    monkeypatch.setattr(\"pdm.project.lockfile.PDMLock.spec_version\", parse_version(\"4.1.1\"))\n    project.lockfile._data[\"metadata\"][\"lock_version\"] = lock_version\n    assert project.lockfile.compatibility() == expected\n    result = pdm([\"lock\", \"--check\"], obj=project)\n    assert result.exit_code == (1 if expected == Compatibility.NONE else 0)\n\n\ndef test_lock_default_inherit_metadata(project, pdm, mocker, working_set):\n    project.add_dependencies([\"requests\"])\n    pdm([\"lock\"], obj=project, strict=True)\n    assert \"inherit_metadata\" in project.lockfile.strategy\n    packages = project.lockfile[\"package\"]\n    assert all(package[\"groups\"] == [\"default\"] for package in packages)\n\n    resolver = mocker.patch.object(project, \"get_resolver\")\n    pdm([\"sync\"], obj=project, strict=True)\n    resolver.assert_not_called()\n    for key in (\"requests\", \"idna\", \"chardet\", \"urllib3\"):\n        assert key in working_set\n\n\ndef test_lock_inherit_metadata_strategy(project, pdm, mocker, working_set):\n    project.add_dependencies([\"requests\"])\n    pdm([\"lock\", \"-S\", \"inherit_metadata\"], obj=project, strict=True)\n    assert \"inherit_metadata\" in project.lockfile.strategy\n    packages = project.lockfile[\"package\"]\n    assert all(package[\"groups\"] == [\"default\"] for package in packages)\n\n    resolver = mocker.patch.object(project, \"get_resolver\")\n    pdm([\"sync\"], obj=project, strict=True)\n    resolver.assert_not_called()\n    for key in (\"requests\", \"idna\", \"chardet\", \"urllib3\"):\n        assert key in working_set\n\n\ndef test_lock_exclude_newer(project, pdm):\n    project.pyproject.metadata[\"requires-python\"] = \">=3.9\"\n    project.project_config[\"pypi.url\"] = \"https://my.pypi.org/json\"\n    project.add_dependencies([\"zipp\"])\n    pdm([\"lock\", \"--exclude-newer\", \"2024-01-01\"], obj=project, strict=True, cleanup=False)\n    assert project.get_locked_repository().candidates[\"zipp\"].version == \"3.6.0\"\n\n    pdm([\"lock\"], obj=project, strict=True, cleanup=False)\n    assert project.get_locked_repository().candidates[\"zipp\"].version == \"3.7.0\"\n\n\nexclusion_cases = [\n    pytest.param((\"-G\", \":all\", \"--without\", \"tz,ssl\"), id=\"-G :all --without tz,ssl\"),\n    pytest.param((\"-G\", \":all\", \"--without\", \"tz\", \"--without\", \"ssl\"), id=\"-G :all --without tz --without ssl\"),\n    pytest.param((\"--with\", \":all\", \"--without\", \"tz,ssl\"), id=\"--with all --without tz,ssl\"),\n    pytest.param((\"--with\", \":all\", \"--without\", \"tz\", \"--without\", \"ssl\"), id=\"--with all --without tz --without ssl\"),\n    pytest.param((\"--without\", \"tz\", \"--without\", \"ssl\"), id=\"--without tz --without ssl\"),\n    pytest.param((\"--without\", \"tz,ssl\"), id=\"--without tz,ssl\"),\n]\n\n\n@pytest.mark.parametrize(\"args\", exclusion_cases)\n@pytest.mark.usefixtures(\"repository\")\ndef test_lock_all_with_excluded_groups(project, pdm, args):\n    project.add_dependencies([\"urllib3\"], \"url\")\n    project.add_dependencies([\"pytz\"], \"tz\", True)\n    project.add_dependencies([\"pyopenssl\"], \"ssl\")\n    pdm([\"lock\", *args], obj=project, strict=True)\n    assert \"urllib3\" in project.get_locked_repository().candidates\n    assert \"pytz\" not in project.get_locked_repository().candidates\n    assert \"pyopenssl\" not in project.get_locked_repository().candidates\n\n\n@pytest.mark.parametrize(\n    \"args\",\n    [\n        (\"--append\",),\n        (\"--python\", \"<3.6\"),\n        (\"-S\", \"cross_platform\", \"--append\", \"--python\", \"3.10\"),\n        (\"--platform\", \"linux\", \"--refresh\"),\n    ],\n)\ndef test_forbidden_lock_target_options(project, pdm, args):\n    result = pdm([\"lock\", *args], obj=project)\n    assert result.exit_code != 0\n    assert \"PdmUsageError\" in result.stderr\n\n\n@pytest.mark.parametrize(\"nested\", [False, True])\ndef test_lock_for_multiple_targets(project, pdm, repository, nested):\n    deps = [\n        'django<2; sys_platform == \"win32\"',\n        'django>=2; sys_platform != \"win32\"',\n    ]\n    if nested:\n        repository.add_candidate(\"foo\", \"0.1.0\")\n        repository.add_dependencies(\"foo\", \"0.1.0\", deps)\n        project.add_dependencies([\"foo\"])\n    else:\n        project.add_dependencies(deps)\n\n    pdm([\"lock\", \"--platform\", \"windows\"], obj=project, strict=True)\n    locked = project.get_locked_repository()\n    candidates = locked.all_candidates\n    assert len(candidates[\"django\"]) == 1\n    assert candidates[\"django\"][0].version == \"1.11.8\"\n    assert len(locked.targets) == 1\n    pytz = candidates[\"pytz\"][0]\n    assert str(pytz.req.marker) == 'sys_platform == \"win32\"'\n\n    result = pdm([\"lock\", \"--platform\", \"windows\", \"--append\"], obj=project, strict=True)\n    assert \"already exists, skip locking.\" in result.stdout\n\n    pdm([\"lock\", \"--platform\", \"linux\", \"--append\"], obj=project, strict=True)\n    locked = project.get_locked_repository()\n    candidates = locked.all_candidates\n    assert len(locked.targets) == 2\n    assert sorted(c.version for c in candidates[\"django\"]) == [\"1.11.8\", \"2.2.9\"]\n    pytz = candidates[\"pytz\"][0]\n    assert not pytz.req.marker or pytz.req.marker.is_any()\n\n    # not append but overwrite\n    pdm([\"lock\", \"--platform\", \"windows\"], obj=project, strict=True)\n    locked = project.get_locked_repository()\n    candidates = locked.all_candidates\n    assert len(candidates[\"django\"]) == 1\n    assert candidates[\"django\"][0].version == \"1.11.8\"\n    assert len(locked.targets) == 1\n    pytz = candidates[\"pytz\"][0]\n    assert str(pytz.req.marker) == 'sys_platform == \"win32\"'\n\n\nCONSTRAINT_FILE = str(FIXTURES / \"constraints.txt\")\n\n\n@pytest.mark.usefixtures(\"repository\")\n@pytest.mark.parametrize(\"constraint\", [CONSTRAINT_FILE, Path(CONSTRAINT_FILE).as_uri()])\ndef test_lock_with_override_file(project, pdm, constraint):\n    project.add_dependencies([\"requests\"])\n    pdm([\"lock\", \"--override\", constraint], obj=project, strict=True)\n    candidates = project.get_locked_repository().candidates\n    assert candidates[\"requests\"].version == \"2.20.0b1\"\n    assert candidates[\"urllib3\"].version == \"1.23b0\"\n    assert \"django\" not in candidates\n\n\ndef test_pylock_add_remove_strategy(project, pdm):\n    project.project_config[\"lock.format\"] = \"pylock\"\n    pdm([\"lock\"], obj=project, strict=True)\n    assert project.lockfile.strategy == {\"inherit_metadata\", \"static_urls\"}\n    pdm([\"lock\", \"-S\", \"static_urls\"], obj=project, strict=True)\n    pdm([\"lock\", \"-S\", \"direct_minimal_versions\"], obj=project, strict=True)\n    assert project.lockfile.strategy == {\"inherit_metadata\", \"static_urls\", \"direct_minimal_versions\"}\n\n    result = pdm([\"lock\", \"-S\", \"no_static_urls\"], obj=project)\n    assert result.exit_code != 0\n    result = pdm([\"lock\", \"-S\", \"no_inherit_metadata\"], obj=project)\n    assert result.exit_code != 0\n\n\n@pytest.mark.usefixtures(\"repository\")\ndef test_lock_with_invalid_python_requirement(project, pdm):\n    project.add_dependencies([\"requests\", \"python>=3.6\"])\n    result = pdm([\"lock\", \"-v\"], obj=project, strict=True)\n    assert \"requests\" in project.get_locked_repository().candidates\n    assert \"python\" not in project.get_locked_repository().candidates\n    assert \"The 'python' requirement is not necessary and will be ignored.\" in result.stderr\n"
  },
  {
    "path": "tests/cli/test_others.py",
    "content": "import json\nfrom pathlib import Path\n\nimport pytest\n\nfrom pdm.cli import actions\nfrom pdm.utils import cd\nfrom tests import FIXTURES\n\n\ndef test_build_distributions(project):\n    from pdm.cli.commands.build import Command\n\n    Command.do_build(project)\n    dist = project.root / \"dist\"\n    assert dist.exists()\n    wheel = next(dist.glob(\"*.whl\"))\n    assert wheel.name.startswith(\"test_project-\")\n    tarball = next(dist.glob(\"*.tar.gz\"))\n    assert tarball.name.startswith(\"test_project-\")\n\n\ndef test_project_no_init_error(project_no_init, pdm):\n    for command in (\"add\", \"lock\", \"update\"):\n        result = pdm([command], obj=project_no_init)\n        assert result.exit_code != 0\n        assert \"The pyproject.toml has not been initialized yet\" in result.stderr\n\n\ndef test_help_option(pdm):\n    result = pdm([\"--help\"])\n    assert \"usage: pdm [-h]\" in result.output.lower()\n\n\ndef test_pep582_option(pdm):\n    result = pdm([\"--pep582\", \"bash\"])\n    assert result.exit_code == 0\n\n\ndef test_info_command(project, pdm):\n    result = pdm([\"info\"], obj=project)\n    assert \"Project Root:\" in result.output\n    assert project.root.as_posix() in result.output\n\n    result = pdm([\"info\", \"--python\"], obj=project)\n    assert result.output.strip() == str(project.python.executable)\n\n    result = pdm([\"info\", \"--where\"], obj=project)\n    assert result.output.strip() == str(project.root)\n\n    result = pdm([\"info\", \"--env\"], obj=project)\n    assert result.exit_code == 0\n\n\ndef test_info_command_json(project, pdm):\n    result = pdm([\"info\", \"--json\"], obj=project, strict=True)\n\n    data = json.loads(result.outputs)\n\n    assert data[\"pdm\"][\"version\"] == project.core.version\n    assert data[\"python\"][\"version\"] == project.environment.interpreter.identifier\n    assert data[\"python\"][\"interpreter\"] == str(project.environment.interpreter.executable)\n    assert isinstance(data[\"python\"][\"markers\"], dict)\n    assert data[\"project\"][\"root\"] == str(project.root)\n    assert isinstance(data[\"project\"][\"pypackages\"], str)\n\n\ndef test_info_global_project(pdm, tmp_path):\n    with cd(tmp_path):\n        result = pdm([\"info\", \"-g\", \"--where\"])\n    assert \"global-project\" in result.output.strip()\n\n\ndef test_info_with_multiple_venvs(pdm, project):\n    project.global_config[\"python.use_venv\"] = True\n    pdm([\"venv\", \"create\"], obj=project, strict=True)\n    pdm([\"venv\", \"create\", \"--name\", \"test\"], obj=project, strict=True)\n    project._saved_python = None\n    result = pdm([\"info\", \"--python\"], obj=project, strict=True)\n    assert Path(result.output.strip()).parent.parent == project.root / \".venv\"\n    venv_location = project.config[\"venv.location\"]\n    result = pdm([\"info\", \"--python\", \"--venv\", \"test\"], obj=project, strict=True)\n    assert Path(result.output.strip()).parent.parent.parent == project.root / venv_location\n\n    result = pdm([\"info\", \"--python\", \"--venv\", \"test\"], obj=project, strict=True, env={\"PDM_IN_VENV\": \"test\"})\n    assert Path(result.output.strip()).parent.parent.parent == project.root / venv_location\n    result = pdm([\"info\", \"--python\", \"--venv\", \"default\"], obj=project)\n    assert \"No virtualenv with key 'default' is found\" in result.stderr\n\n\ndef test_global_project_other_location(pdm, project):\n    result = pdm([\"info\", \"-g\", \"-p\", project.root.as_posix(), \"--where\"])\n    assert result.stdout.strip() == str(project.root)\n\n\ndef test_uncaught_error(pdm, mocker):\n    mocker.patch.object(actions, \"do_lock\", side_effect=RuntimeError(\"test error\"))\n    result = pdm([\"lock\"])\n    assert \"[RuntimeError]: test error\" in result.stderr\n\n    result = pdm([\"lock\", \"-v\"])\n    assert isinstance(result.exception, RuntimeError)\n\n\n@pytest.mark.parametrize(\n    \"filename\",\n    [\n        \"requirements.txt\",\n        \"Pipfile\",\n        \"pyproject.toml\",\n        \"projects/flit-demo/pyproject.toml\",\n    ],\n)\ndef test_import_other_format_file(project, pdm, filename):\n    requirements_file = FIXTURES / filename\n    result = pdm([\"import\", str(requirements_file)], obj=project)\n    assert result.exit_code == 0\n\n\ndef test_import_requirement_no_overwrite(project, pdm, tmp_path):\n    project.add_dependencies([\"requests\"])\n    tmp_path.joinpath(\"reqs.txt\").write_text(\"flask\\nflask-login\\n\")\n    result = pdm([\"import\", \"-dGweb\", str(tmp_path.joinpath(\"reqs.txt\"))], obj=project)\n    assert result.exit_code == 0, result.stderr\n    assert [r.key for r in project.get_dependencies()] == [\"requests\"]\n    assert [r.key for r in project.get_dependencies(\"web\")] == [\"flask\", \"flask-login\"]\n\n\n@pytest.mark.network\n@pytest.mark.xfail(reason=\"HTTP search risk controlled\")\ndef test_search_package(pdm, tmp_path):\n    with cd(tmp_path):\n        result = pdm([\"search\", \"requests\"], strict=True)\n    assert len(result.output.splitlines()) > 0\n    assert not tmp_path.joinpath(\"__pypackages__\").exists()\n    assert not tmp_path.joinpath(\".pdm-python\").exists()\n\n\n@pytest.mark.network\ndef test_show_package_on_pypi(pdm):\n    result = pdm([\"show\", \"ipython\"])\n    assert result.exit_code == 0\n    assert \"ipython\" in result.output.splitlines()[0]\n\n    result = pdm([\"show\", \"requests\"])\n    assert result.exit_code == 0\n    assert \"requests\" in result.output.splitlines()[0]\n\n    result = pdm([\"show\", \"--name\", \"requests\"])\n    assert result.exit_code == 0\n    assert \"requests\" in result.output.splitlines()[0]\n\n    result = pdm([\"show\", \"--name\", \"sphinx-data-viewer\"])\n    assert result.exit_code == 0\n    assert \"sphinx-data-viewer\" in result.output.splitlines()[0]\n\n\ndef test_show_self_package(project, pdm):\n    result = pdm([\"show\"], obj=project)\n    assert result.exit_code == 0, result.stderr\n\n    result = pdm([\"show\", \"--name\", \"--version\"], obj=project)\n    assert result.exit_code == 0\n    assert \"test-project\\n0.0.0\\n\" == result.output\n\n\ndef test_export_to_requirements_txt(pdm, fixture_project):\n    project = fixture_project(\"demo-package\")\n    requirements_txt = project.root / \"requirements.txt\"\n    requirements_no_hashes = project.root / \"requirements_simple.txt\"\n    requirements_pyproject = project.root / \"requirements.ini\"\n\n    result = pdm([\"export\"], obj=project)\n    assert result.exit_code == 0\n    assert result.output.strip() == requirements_txt.read_text().strip()\n\n    result = pdm([\"export\", \"--self\"], obj=project)\n    assert result.exit_code == 1\n\n    result = pdm([\"export\", \"--editable-self\"], obj=project)\n    assert result.exit_code == 1\n\n    result = pdm([\"export\", \"--no-hashes\", \"--self\"], obj=project)\n    assert result.exit_code == 0\n    assert \".  # this package\\n\" in result.output.strip()\n\n    result = pdm([\"export\", \"--no-hashes\", \"--editable-self\"], obj=project)\n    assert result.exit_code == 0\n    assert \"-e .  # this package\\n\" in result.output.strip()\n\n    result = pdm([\"export\", \"--no-hashes\"], obj=project)\n    assert result.exit_code == 0\n    assert result.output.strip() == requirements_no_hashes.read_text().strip()\n\n    result = pdm([\"export\", \"--pyproject\"], obj=project)\n    assert result.exit_code == 0\n    assert result.output.strip() == requirements_pyproject.read_text().strip()\n\n    result = pdm([\"export\", \"-o\", str(project.root / \"requirements_output.txt\")], obj=project)\n    assert result.exit_code == 0\n    assert (project.root / \"requirements_output.txt\").read_text() == requirements_txt.read_text()\n\n\n@pytest.mark.parametrize(\"extra_opt\", [[], [\"--no-extras\"]])\ndef test_export_doesnt_include_dep_with_extras(pdm, fixture_project, extra_opt):\n    project = fixture_project(\"demo-package-has-dep-with-extras\")\n\n    result = pdm([\"export\", \"--without-hashes\", *extra_opt], obj=project)\n    assert result.exit_code == 0\n    if extra_opt:\n        assert \"requests==2.26.0\" in result.output.splitlines()\n    else:\n        assert \"requests[security]==2.26.0\" in result.output.splitlines()\n\n\ndef test_completion_command(pdm):\n    result = pdm([\"completion\", \"bash\"])\n    assert result.exit_code == 0\n    assert \"(completion)\" in result.output\n\n\n@pytest.mark.network\ndef test_show_update_hint(pdm, project, monkeypatch):\n    monkeypatch.delenv(\"PDM_CHECK_UPDATE\", raising=False)\n    prev_version = project.core.version\n    try:\n        project.core.version = \"0.0.0\"\n        r = pdm([\"config\"], obj=project)\n    finally:\n        project.core.version = prev_version\n    assert \"to upgrade.\" in r.stderr\n    assert \"Run `pdm config check_update false` to disable the check.\" in r.stderr\n\n\n@pytest.mark.usefixtures(\"repository\")\ndef test_export_with_platform_markers(pdm, project):\n    pdm(\n        [\"add\", \"--no-sync\", 'urllib3; sys_platform == \"fake\"', 'idna; python_version >= \"3.7\"'],\n        obj=project,\n        strict=True,\n    )\n    result = pdm([\"export\", \"--no-hashes\"], obj=project, strict=True)\n    result_lines = result.output.splitlines()\n    assert 'urllib3==1.22; sys_platform == \"fake\"' in result_lines\n    assert 'idna==2.7; python_version >= \"3.7\"' in result_lines\n\n    result = pdm([\"export\", \"--no-hashes\", \"--no-markers\"], obj=project, strict=True)\n    result_lines = result.output.splitlines()\n    assert not any(line.startswith(\"urllib3\") for line in result_lines)\n    assert \"idna==2.7\" in result_lines\n\n\n@pytest.mark.usefixtures(\"repository\", \"vcs\")\ndef test_export_with_vcs_deps(pdm, project):\n    pdm([\"add\", \"--no-sync\", \"git+https://github.com/test-root/demo.git\"], obj=project, strict=True)\n    result = pdm([\"export\"], obj=project)\n    assert result.exit_code != 0\n\n    result = pdm([\"export\", \"--no-hashes\"], obj=project)\n    assert result.exit_code == 0\n    assert \"demo @ git+https://github.com/test-root/demo.git@1234567890abcdef\" in result.output.splitlines()\n\n\n@pytest.mark.usefixtures(\"repository\")\ndef test_keep_log_on_failure(pdm, project, tmp_path):\n    log_dir = tmp_path / \"logs\"\n    project.global_config[\"log_dir\"] = str(log_dir)\n    pdm([\"add\", \"non_exist_package\"], obj=project)\n    log_files = list(log_dir.glob(\"pdm-lock-*.log\"))\n    assert len(log_files) == 1\n\n\n@pytest.mark.usefixtures(\"repository\")\ndef test_truncated_log_on_failure(pdm, project, tmp_path, monkeypatch):\n    monkeypatch.setattr(\"pdm.termui.UI.MAX_LOG_SIZE\", 100)  # 100 bytes\n    project.global_config[\"log_dir\"] = str(tmp_path / \"logs\")\n    pdm([\"add\", \"non_exist_package\"], obj=project)\n    log_file = next((tmp_path / \"logs\").glob(\"pdm-lock-*.log\"))\n    assert log_file.stat().st_size < 200\n    assert \"[truncated]\" in log_file.read_text()\n"
  },
  {
    "path": "tests/cli/test_outdated.py",
    "content": "import json\nfrom unittest import mock\n\nimport pytest\nfrom rich.box import ASCII\n\n\n@mock.patch(\"pdm.termui.ROUNDED\", ASCII)\n@pytest.mark.usefixtures(\"working_set\")\ndef test_outdated(project, pdm, index):\n    pdm([\"add\", \"requests\"], obj=project, strict=True, cleanup=False)\n    project.project_config[\"pypi.url\"] = \"https://my.pypi.org/simple\"\n    del project.pyproject.settings[\"source\"]\n    project.pyproject.write()\n    index[\"/simple/requests/\"] = b\"\"\"\\\n<!DOCTYPE html>\n<html>\n  <body>\n    <h1>requests</h1>\n    <a\n      href=\"http://fixtures.test/artifacts/requests-2.20.0-py3-none-any.whl\"\n      data-requires-python=\">=3.7\"\n    >\n      requests-2.20.0-py3-none-any.whl\n    </a>\n  </body>\n</html>\n\n\"\"\"\n\n    result = pdm([\"outdated\"], obj=project, strict=True, cleanup=False)\n    assert \"| requests | default | 2.19.1    | 2.19.1 | 2.20.0 |\" in result.stdout\n\n    result = pdm([\"outdated\", \"re*\"], obj=project, strict=True, cleanup=False)\n    assert \"| requests | default | 2.19.1    | 2.19.1 | 2.20.0 |\" in result.stdout\n\n    result = pdm([\"outdated\", \"--json\"], obj=project, strict=True, cleanup=False)\n    json_output = json.loads(result.stdout)\n    assert json_output == [\n        {\n            \"package\": \"requests\",\n            \"groups\": [\"default\"],\n            \"installed_version\": \"2.19.1\",\n            \"pinned_version\": \"2.19.1\",\n            \"latest_version\": \"2.20.0\",\n        }\n    ]\n"
  },
  {
    "path": "tests/cli/test_publish.py",
    "content": "import base64\nimport os\nfrom argparse import Namespace\n\nimport pytest\n\nfrom pdm._types import RepositoryConfig\nfrom pdm.cli.commands.publish import Command as PublishCommand\nfrom pdm.cli.commands.publish.package import PackageFile\nfrom pdm.cli.commands.publish.repository import Repository\nfrom pdm.exceptions import PdmUsageError\nfrom tests import FIXTURES\n\npytestmark = pytest.mark.usefixtures(\"mock_run_gpg\")\n\n\n@pytest.mark.parametrize(\n    \"filename\",\n    [\"demo-0.0.1-py2.py3-none-any.whl\", \"demo-0.0.1.tar.gz\", \"demo-0.0.1.zip\"],\n)\ndef test_package_parse_metadata(filename):\n    fullpath = FIXTURES / \"artifacts\" / filename\n    package = PackageFile.from_filename(str(fullpath), None)\n    assert package.base_filename == filename\n    meta = package.metadata_dict\n    assert meta[\"name\"] == \"demo\"\n    assert meta[\"version\"] == \"0.0.1\"\n    assert all(f\"{hash_name}_digest\" in meta for hash_name in [\"md5\", \"sha256\", \"blake2_256\"])\n\n    if filename.endswith(\".whl\"):\n        assert meta[\"pyversion\"] == \"py2.py3\"\n        assert meta[\"filetype\"] == \"bdist_wheel\"\n    else:\n        assert meta[\"pyversion\"] == \"source\"\n        assert meta[\"filetype\"] == \"sdist\"\n\n\ndef test_parse_metadata_with_non_ascii_chars():\n    fullpath = FIXTURES / \"artifacts\" / \"caj2pdf-restructured-0.1.0a6.tar.gz\"\n    package = PackageFile.from_filename(str(fullpath), None)\n    meta = package.metadata_dict\n    assert meta[\"summary\"] == \"caj2pdf 重新组织，方便打包与安装\"  # noqa: RUF001\n    assert meta[\"author_email\"] == \"张三 <san@zhang.me>\"\n    assert meta[\"description\"].strip() == \"# caj2pdf\\n\\n测试中文项目\"\n\n\ndef test_package_add_signature(tmp_path):\n    package = PackageFile.from_filename(str(FIXTURES / \"artifacts/demo-0.0.1-py2.py3-none-any.whl\"), None)\n    tmp_path.joinpath(\"signature.asc\").write_bytes(b\"test gpg signature\")\n    package.add_gpg_signature(str(tmp_path / \"signature.asc\"), \"signature.asc\")\n    assert package.gpg_signature == (\"signature.asc\", b\"test gpg signature\")\n\n\ndef test_package_call_gpg_sign():\n    package = PackageFile.from_filename(str(FIXTURES / \"artifacts/demo-0.0.1-py2.py3-none-any.whl\"), None)\n    try:\n        package.sign(None)\n    finally:\n        try:\n            os.unlink(package.filename + \".asc\")\n        except OSError:\n            pass\n    assert package.gpg_signature == (package.base_filename + \".asc\", b\"fake signature\")\n\n\ndef test_repository_get_release_urls(project):\n    package_files = [\n        PackageFile.from_filename(str(FIXTURES / \"artifacts\" / fn), None)\n        for fn in [\n            \"demo-0.0.1-py2.py3-none-any.whl\",\n            \"demo-0.0.1.tar.gz\",\n            \"demo-0.0.1.zip\",\n        ]\n    ]\n    config = RepositoryConfig(\n        config_prefix=\"repository\",\n        name=\"test\",\n        url=\"https://upload.pypi.org/legacy/\",\n        username=\"abc\",\n        password=\"123\",\n    )\n    repository = Repository(project, config)\n    assert repository.get_release_urls(package_files) == {\"https://pypi.org/project/demo/0.0.1/\"}\n\n    repository.url = \"https://example.pypi.org/legacy/\"\n    assert not repository.get_release_urls(package_files)\n\n\n@pytest.mark.usefixtures(\"prepare_packages\")\ndef test_publish_pick_up_asc_files(project, uploaded, pdm):\n    for p in list(project.root.joinpath(\"dist\").iterdir()):\n        with open(str(p) + \".asc\", \"w\") as f:\n            f.write(\"fake signature\")\n\n    pdm(\n        [\"publish\", \"--no-build\", \"--username=abc\", \"--password=123\"],\n        obj=project,\n        strict=True,\n    )\n    # Test wheels are uploaded first\n    assert uploaded[0].base_filename.endswith(\".whl\")\n    for package in uploaded:\n        assert package.gpg_signature == (\n            package.base_filename + \".asc\",\n            b\"fake signature\",\n        )\n\n\n@pytest.mark.usefixtures(\"prepare_packages\")\ndef test_publish_package_with_signature(project, uploaded, pdm):\n    pdm(\n        [\"publish\", \"--no-build\", \"-S\", \"--username=abc\", \"--password=123\"],\n        obj=project,\n        strict=True,\n    )\n    for package in uploaded:\n        assert package.gpg_signature == (\n            package.base_filename + \".asc\",\n            b\"fake signature\",\n        )\n\n\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_publish_and_build_in_one_run(fixture_project, pdm, mock_pypi):\n    project = fixture_project(\"demo-module\")\n    result = pdm([\"publish\", \"--username=abc\", \"--password=123\"], obj=project, strict=True).output\n\n    mock_pypi.assert_called()\n    assert \"Uploading demo_module-0.1.0-py3-none-any.whl\" in result\n    assert \"Uploading demo_module-0.1.0.tar.gz\" in result, result\n    assert \"https://pypi.org/project/demo-module/0.1.0/\" in result\n\n\ndef test_publish_cli_args_and_env_var_precedence(project, monkeypatch, mocker):\n    repository = mocker.patch.object(Repository, \"__init__\", return_value=None)\n    PublishCommand.get_repository(\n        project,\n        Namespace(\n            repository=None,\n            username=\"foo\",\n            password=\"bar\",\n            ca_certs=\"custom.pem\",\n            verify_ssl=True,\n        ),\n    )\n    repository.assert_called_with(\n        project,\n        RepositoryConfig(\n            config_prefix=\"repository\",\n            name=\"pypi\",\n            url=\"https://upload.pypi.org/legacy/\",\n            username=\"foo\",\n            password=\"bar\",\n            ca_certs=\"custom.pem\",\n            verify_ssl=None,\n        ),\n    )\n\n    with monkeypatch.context() as m:\n        m.setenv(\"PDM_PUBLISH_USERNAME\", \"bar\")\n        m.setenv(\"PDM_PUBLISH_PASSWORD\", \"secret\")\n        m.setenv(\"PDM_PUBLISH_REPO\", \"testpypi\")\n        m.setenv(\"PDM_PUBLISH_CA_CERTS\", \"override.pem\")\n\n        PublishCommand.get_repository(\n            project,\n            Namespace(\n                repository=None,\n                username=None,\n                password=None,\n                ca_certs=None,\n                verify_ssl=True,\n            ),\n        )\n        repository.assert_called_with(\n            project,\n            RepositoryConfig(\n                config_prefix=\"repository\",\n                name=\"testpypi\",\n                url=\"https://test.pypi.org/legacy/\",\n                username=\"bar\",\n                password=\"secret\",\n                ca_certs=\"override.pem\",\n                verify_ssl=None,\n            ),\n        )\n\n        PublishCommand.get_repository(\n            project,\n            Namespace(\n                repository=\"pypi\",\n                username=\"foo\",\n                password=None,\n                ca_certs=\"custom.pem\",\n                verify_ssl=True,\n            ),\n        )\n        repository.assert_called_with(\n            project,\n            RepositoryConfig(\n                config_prefix=\"repository\",\n                name=\"pypi\",\n                url=\"https://upload.pypi.org/legacy/\",\n                username=\"foo\",\n                password=\"secret\",\n                ca_certs=\"custom.pem\",\n                verify_ssl=None,\n            ),\n        )\n\n\ndef test_repository_get_credentials_from_keyring(project, keyring, mocker):\n    keyring.save_auth_info(\"https://test.org/upload\", \"foo\", \"barbaz\")\n    config = RepositoryConfig(config_prefix=\"repository\", name=\"test\", url=\"https://test.org/upload\")\n    basic_auth = mocker.patch(\"httpx.BasicAuth.__init__\", return_value=None)\n    Repository(project, config)\n    basic_auth.assert_called_with(username=\"foo\", password=\"barbaz\")\n\n\ndef test_repository_get_token_from_oidc(project, mocker, httpx_mock):\n    minted_token = \"minted_oidc_token\"\n    test_pypi_url = \"https://test.org/upload\"\n    httpx_mock.add_response(\n        url=\"https://test.org/_/oidc/audience\",\n        method=\"GET\",\n        json={\"audience\": \"testpypi\"},\n    )\n    httpx_mock.add_response(\n        url=\"https://test.org/_/oidc/mint-token\",\n        method=\"POST\",\n        json={\"token\": minted_token},\n    )\n    config = RepositoryConfig(config_prefix=\"repository\", name=\"test\", url=test_pypi_url)\n    detect_credential_mock = mocker.patch(\n        \"pdm.cli.commands.publish.repository.detect_credential\",\n        return_value=\"A_OIDC_TOKEN\",\n    )\n    repository = Repository(project, config=config)\n    detect_credential_mock.assert_called_once()\n    assert base64.b64decode(repository.session.auth._auth_header[6:]).decode(\"utf-8\") == f\"__token__:{minted_token}\"\n\n\ndef test_repository_get_token_from_oidc_request_error(project, mocker, httpx_mock, capsys):\n    test_pypi_url = \"https://test.org/upload\"\n    httpx_mock.add_response(\n        url=\"https://test.org/_/oidc/audience\",\n        method=\"GET\",\n        status_code=502,\n    )\n\n    config = RepositoryConfig(config_prefix=\"repository\", name=\"test\", url=test_pypi_url)\n    with pytest.raises(PdmUsageError):\n        Repository(project, config=config)\n    captured = capsys.readouterr()\n    assert \"Failed to get PyPI token via OIDC\" in captured.err\n\n\ndef test_repository_get_token_from_oidc_unsupported_platform(project, mocker, httpx_mock, capsys):\n    test_pypi_url = \"https://test.org/upload\"\n    httpx_mock.add_response(\n        url=\"https://test.org/_/oidc/audience\",\n        method=\"GET\",\n        json={\"audience\": \"testpypi\"},\n    )\n\n    config = RepositoryConfig(config_prefix=\"repository\", name=\"test\", url=test_pypi_url)\n    detect_credential_mock = mocker.patch(\n        \"pdm.cli.commands.publish.repository.detect_credential\",\n        return_value=None,\n    )\n    with pytest.raises(PdmUsageError):\n        Repository(project, config=config)\n    captured = capsys.readouterr()\n    assert \"This platform is not supported for trusted publishing via OIDC\" in captured.err\n    detect_credential_mock.assert_called_once()\n\n\ndef test_repository_get_token_misconfigured_github(project, monkeypatch, capsys, httpx_mock):\n    test_pypi_url = \"https://test.org/upload\"\n    httpx_mock.add_response(\n        url=\"https://test.org/_/oidc/audience\",\n        method=\"GET\",\n        json={\"audience\": \"testpypi\"},\n    )\n\n    config = RepositoryConfig(config_prefix=\"repository\", name=\"test\", url=test_pypi_url)\n    # set env variable to get `id`` into detect_github function\n    monkeypatch.setenv(\"GITHUB_ACTIONS\", \"true\")\n\n    with pytest.raises(PdmUsageError):\n        Repository(project, config=config)\n    captured = capsys.readouterr()\n    assert \"Unable to detect OIDC token for CI platform:\" in captured.err\n"
  },
  {
    "path": "tests/cli/test_python.py",
    "content": "import platform\nimport sys\nfrom pathlib import Path\n\nimport pytest\nfrom pbs_installer import PythonVersion\n\nfrom pdm.models.python import PythonInfo\nfrom pdm.utils import parse_version\n\n\n@pytest.fixture\ndef mock_install(mocker):\n    if (arch := platform.machine().lower()) not in (\"arm64\", \"aarch64\", \"amd64\", \"x86_64\"):\n        pytest.skip(f\"Skipped on non-standard platform: {arch}\")\n\n    def install_file(\n        filename,\n        destination,\n        original_filename=None,\n        build_dir=False,\n    ) -> None:\n        if sys.platform == \"win32\":\n            Path(destination, \"python.exe\").touch()\n        else:\n            Path(destination, \"bin\").mkdir(parents=True, exist_ok=True)\n            Path(destination, \"bin\", \"python3\").touch()\n\n    def get_download_link(version, *args, **kwargs):\n        return f\"cpython@{version}\", None\n\n    def get_version(self):\n        name = self.executable.parent.name if sys.platform == \"win32\" else self.executable.parent.parent.name\n        if \"@\" not in name:\n            return parse_version(platform.python_version())\n        return parse_version(name.split(\"@\", 1)[1].rstrip(\"t\"))\n\n    def get_freethreaded(self):\n        name = self.executable.parent.name if sys.platform == \"win32\" else self.executable.parent.parent.name\n        return name.endswith(\"t\")\n\n    @property\n    def interpreter(self):\n        return self.executable\n\n    @property\n    def implementation(self):\n        name = self.executable.parent.name if sys.platform == \"win32\" else self.executable.parent.parent.name\n        if \"@\" not in name:\n            return \"cpython\"\n        return name.split(\"@\", 1)[0]\n\n    mocker.patch(\"pbs_installer.download\", return_value=\"python-3.10.8.tar.gz\")\n    mocker.patch(\"pbs_installer.get_download_link\", side_effect=get_download_link)\n    installer = mocker.patch(\"pbs_installer.install_file\", side_effect=install_file)\n    mocker.patch(\"findpython.python.PythonVersion.implementation\", implementation)\n    mocker.patch(\"findpython.python.PythonVersion._get_version\", get_version)\n    mocker.patch(\"findpython.python.PythonVersion._get_freethreaded\", get_freethreaded)\n    mocker.patch(\"findpython.python.PythonVersion.interpreter\", interpreter)\n    mocker.patch(\"findpython.python.PythonVersion.architecture\", mocker.PropertyMock(return_value=\"64bit\"))\n    return installer\n\n\ndef test_install_python(project, pdm, mock_install):\n    root = Path(project.config[\"python.install_root\"])\n\n    pdm([\"py\", \"install\", \"cpython@3.10.8\", \"-v\"], obj=project, strict=True)\n    mock_install.assert_called_once()\n    assert (root / \"cpython@3.10.8\").exists()\n\n    result = pdm([\"py\", \"list\"], obj=project, strict=True)\n    assert result.stdout.splitlines()[0].startswith(\"cpython@3.10.8\")\n\n    result = pdm([\"py\", \"remove\", \"3.11.1\"], obj=project)\n    assert result.exit_code != 0\n    pdm([\"py\", \"remove\", \"cpython@3.10.8\"], obj=project, strict=True)\n    assert not (root / \"cpython@3.10.8\").exists()\n\n    result = pdm([\"py\", \"install\", \"--list\"], obj=project, strict=True)\n    assert len(result.stdout.splitlines()) > 0\n\n\ndef test_install_python_best_match(project, pdm, mock_install, mocker):\n    root = Path(project.config[\"python.install_root\"])\n    mock_best_match = mocker.patch(\n        \"pdm.project.core.Project.get_best_matching_cpython_version\", return_value=PythonVersion(\"cpython\", 3, 10, 8)\n    )\n\n    pdm([\"py\", \"install\"], obj=project, strict=True)\n    mock_best_match.assert_called_once()\n    mock_install.assert_called_once()\n    assert (root / \"cpython@3.10.8\").exists()\n\n\ndef test_install_python_min_match(project, pdm, mock_install, mocker):\n    root = Path(project.config[\"python.install_root\"])\n    mock_best_match = mocker.patch(\n        \"pdm.project.core.Project.get_best_matching_cpython_version\", return_value=PythonVersion(\"cpython\", 3, 10, 7)\n    )\n\n    pdm([\"py\", \"install\", \"--min\"], obj=project, strict=True)\n    mock_best_match.assert_called_once_with(True)\n    mock_install.assert_called_once()\n    assert (root / \"cpython@3.10.7\").exists()\n\n\ndef test_use_auto_install_missing(project, pdm, mock_install, mocker):\n    root = Path(project.config[\"python.install_root\"])\n    mock_find_interpreters = mocker.patch(\"pdm.project.Project.find_interpreters\", return_value=[])\n    mock_best_match = mocker.patch(\"pdm.project.core.Project.get_best_matching_cpython_version\")\n\n    pdm([\"use\", \"3.10.8\"], obj=project, strict=True)\n    mock_install.assert_called_once()\n    mock_find_interpreters.assert_called_once()\n    mock_best_match.assert_not_called()\n    assert (root / \"cpython@3.10.8\").exists()\n\n\ndef test_use_auto_install_pick_latest(project, pdm, mock_install, mocker):\n    root = Path(project.config[\"python.install_root\"])\n    mock_find_interpreters = mocker.patch(\"pdm.project.Project.find_interpreters\", return_value=[])\n    mock_best_match = mocker.patch(\n        \"pdm.project.core.Project.get_best_matching_cpython_version\", return_value=PythonVersion(\"cpython\", 3, 10, 8)\n    )\n\n    pdm([\"use\", \"-v\"], obj=project, strict=True)\n    mock_install.assert_called_once()\n    mock_find_interpreters.assert_called_once()\n    mock_best_match.assert_called_once()\n    assert len(list(root.iterdir())) == 1\n\n\ndef test_use_no_auto_install(project, pdm, mocker):\n    installer = mocker.patch(\"pbs_installer.install_file\")\n    mock_best_match = mocker.patch(\"pdm.project.core.Project.get_best_matching_cpython_version\")\n\n    pdm([\"use\", \"-f\"], obj=project, strict=True)\n    installer.assert_not_called()\n    mock_best_match.assert_not_called()\n\n\ndef test_use_auto_install_strategy_max(project, pdm, mock_install, mocker):\n    root = Path(project.config[\"python.install_root\"])\n    mock_find_interpreters = mocker.patch(\"pdm.project.Project.find_interpreters\")\n    mock_best_match = mocker.patch(\n        \"pdm.project.core.Project.get_best_matching_cpython_version\", return_value=PythonVersion(\"cpython\", 3, 10, 8)\n    )\n\n    pdm([\"use\", \"--auto-install-max\"], obj=project, strict=True)\n    mock_install.assert_called_once()\n    mock_find_interpreters.assert_not_called()\n    mock_best_match.assert_called_once()\n    assert len(list(root.iterdir())) == 1\n\n\ndef test_use_auto_install_strategy_min(project, pdm, mock_install, mocker):\n    root = Path(project.config[\"python.install_root\"])\n    mock_find_interpreters = mocker.patch(\"pdm.project.Project.find_interpreters\")\n    mock_best_match = mocker.patch(\n        \"pdm.project.core.Project.get_best_matching_cpython_version\", return_value=PythonVersion(\"cpython\", 3, 10, 7)\n    )\n\n    pdm([\"use\", \"--auto-install-min\"], obj=project, strict=True)\n    mock_install.assert_called_once()\n    mock_find_interpreters.assert_not_called()\n    mock_best_match.assert_called_once_with(True)\n    assert len(list(root.iterdir())) == 1\n\n\ndef test_link_python(project, pdm):\n    root = Path(project.config[\"python.install_root\"])\n    pdm([\"python\", \"link\", sys.executable], obj=project, strict=True)\n    python_info = PythonInfo.from_path(sys.executable)\n    link_name = f\"{python_info.implementation}@{python_info.identifier}\"\n    assert (root / link_name).resolve() == Path(sys.prefix).resolve()\n\n    pdm([\"python\", \"remove\", link_name], obj=project, strict=True)\n    assert not (root / link_name).exists()\n\n    pdm([\"python\", \"link\", sys.executable, \"--name\", \"foo\"], obj=project, strict=True)\n    assert (root / \"foo\").resolve() == Path(sys.prefix).resolve()\n\n    pdm([\"python\", \"remove\", \"foo\"], obj=project, strict=True)\n    assert not (root / \"foo\").exists()\n\n\ndef test_link_python_invalid_interpreter(project, pdm):\n    result = pdm([\"python\", \"link\", \"/path/to/invalid/python\"], obj=project)\n    assert result.exit_code != 0\n    assert \"Invalid Python interpreter\" in result.stderr\n\n    root = Path(project.config[\"python.install_root\"])\n    root.mkdir(parents=True, exist_ok=True)\n    root.joinpath(\"foo\").touch()\n    result = pdm([\"python\", \"link\", sys.executable, \"--name\", \"foo\"], obj=project)\n    assert result.exit_code != 0\n    assert \"Link foo already exists\" in result.stderr\n\n\ndef test_find_python(project, pdm, mock_install):\n    pdm([\"py\", \"install\", \"3.10.8\"], obj=project, strict=True)\n    pdm([\"py\", \"install\", \"3.13.0\"], obj=project, strict=True)\n    pdm([\"py\", \"install\", \"3.13.0t\"], obj=project, strict=True)\n    result = pdm([\"py\", \"find\", \"3.10.8\"], obj=project, strict=True)\n    assert \"3.10.8\" in result.stdout\n\n    result = pdm([\"py\", \"find\", \"3.10.8\", \"--managed\"], obj=project, strict=True)\n    assert \"3.10.8\" in result.stdout\n\n    result = pdm([\"py\", \"find\", \"3.10.9\"], obj=project)\n    assert result.exit_code != 0\n\n    result = pdm([\"py\", \"find\", \"3.13\", \"--managed\"], obj=project)\n    assert \"3.13.0t\" not in result.stdout\n    assert \"3.13.0\" in result.stdout\n\n    result = pdm([\"py\", \"find\", \"3.13t\", \"--managed\"], obj=project)\n    assert \"3.13.0t\" in result.stdout\n"
  },
  {
    "path": "tests/cli/test_remove.py",
    "content": "import pytest\n\nfrom pdm.cli import actions\nfrom pdm.models.specifiers import PySpecSet\n\n\ndef test_remove_command(project, pdm, mocker):\n    do_remove = mocker.patch(\"pdm.cli.commands.remove.Command.do_remove\")\n    pdm([\"remove\", \"demo\"], obj=project)\n    do_remove.assert_called_once()\n\n\n@pytest.mark.usefixtures(\"working_set\", \"vcs\")\ndef test_remove_editable_packages_while_keeping_normal(project, pdm):\n    project.environment.python_requires = PySpecSet(\">=3.6\")\n    pdm([\"add\", \"demo\"], obj=project, strict=True)\n    pdm([\"add\", \"-d\", \"-e\", \"git+https://github.com/test-root/demo.git#egg=demo\"], obj=project, strict=True)\n    pdm([\"remove\", \"-d\", \"demo\"], obj=project, strict=True)\n    default_group = project.pyproject.metadata[\"dependencies\"]\n    dev_group = project.pyproject.dev_dependencies.get(\"dev\")\n    assert not dev_group\n    assert len(default_group) == 1\n    assert not project.get_locked_repository().candidates[\"demo\"].req.editable\n\n\ndef test_remove_package(project, working_set, dev_option, pdm):\n    pdm([\"add\", *dev_option, \"requests\", \"pytz\"], obj=project, strict=True)\n    pdm([\"remove\", *dev_option, \"pytz\"], obj=project, strict=True)\n    locked_candidates = project.get_locked_repository().candidates\n    assert \"pytz\" not in locked_candidates\n    assert \"pytz\" not in working_set\n\n\ndef test_remove_package_no_lock(project, working_set, dev_option, pdm):\n    pdm([\"add\", *dev_option, \"requests\", \"pytz\"], obj=project, strict=True)\n    pdm([\"remove\", *dev_option, \"--frozen-lockfile\", \"pytz\"], obj=project, strict=True)\n    assert \"pytz\" not in working_set\n    project.lockfile.reload()\n    locked_candidates = project.get_locked_repository().candidates\n    assert \"pytz\" in locked_candidates\n\n\ndef test_remove_package_with_dry_run(project, working_set, pdm):\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    result = pdm([\"remove\", \"requests\", \"--dry-run\"], obj=project, strict=True)\n    project._lockfile = None\n    locked_candidates = project.get_locked_repository().candidates\n    assert \"urllib3\" in locked_candidates\n    assert \"urllib3\" in working_set\n    assert \"- urllib3 1.22\" in result.output\n\n\ndef test_remove_package_no_sync(project, working_set, pdm):\n    pdm([\"add\", \"requests\", \"pytz\"], obj=project, strict=True)\n    pdm([\"remove\", \"pytz\", \"--no-sync\"], obj=project, strict=True)\n    locked_candidates = project.get_locked_repository().candidates\n    assert \"pytz\" not in locked_candidates\n    assert \"pytz\" in working_set\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_remove_package_not_exist(project, pdm):\n    pdm([\"add\", \"requests\", \"pytz\"], obj=project, strict=True)\n    result = pdm([\"remove\", \"django\"], obj=project)\n    assert result.exit_code == 1\n\n\ndef test_remove_package_exist_in_multi_groups(project, working_set, pdm):\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    pdm([\"add\", \"--dev\", \"urllib3\"], obj=project, strict=True)\n    pdm([\"remove\", \"--dev\", \"urllib3\"], obj=project, strict=True)\n    assert \"dependency-groups\" not in project.pyproject._data\n    assert \"urllib3\" in working_set\n    assert \"requests\" in working_set\n\n\n@pytest.mark.usefixtures(\"repository\")\ndef test_remove_no_package(project, pdm):\n    result = pdm([\"remove\"], obj=project)\n    assert result.exit_code != 0\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_remove_package_wont_break_toml(project_no_init, pdm):\n    project_no_init.pyproject._path.write_text(\n        \"\"\"\n[project]\ndependencies = [\n    \"requests\",\n    # this is a comment\n]\n\"\"\"\n    )\n    project_no_init.pyproject.reload()\n    pdm([\"remove\", \"requests\"], obj=project_no_init, strict=True)\n    assert project_no_init.pyproject.metadata[\"dependencies\"] == []\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_remove_group_not_in_lockfile(project, pdm, mocker):\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    project.add_dependencies([\"pytz\"], to_group=\"tz\")\n    assert project.lockfile.groups == [\"default\"]\n    locker = mocker.patch.object(actions, \"do_lock\")\n    pdm([\"remove\", \"--group\", \"tz\", \"pytz\"], obj=project, strict=True)\n    assert \"optional-dependencies\" not in project.pyproject.metadata\n    locker.assert_not_called()\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_remove_exclude_non_existing_dev_group_in_lockfile(project, pdm):\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    project.add_dependencies([\"pytz\"], to_group=\"tz\", dev=True)\n    assert project.lockfile.groups == [\"default\"]\n    result = pdm([\"remove\", \"requests\"], obj=project)\n    assert result.exit_code == 0\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_remove_package_with_group_include(project, pdm):\n    project.pyproject._data[\"dependency-groups\"] = {\n        \"web\": [\"requests\"],\n        \"serve\": [{\"include-group\": \"web\"}, \"django\"],\n    }\n    project.pyproject.write()\n    pdm([\"lock\"], obj=project, strict=True)\n    pdm([\"remove\", \"--no-sync\", \"-Gserve\", \"django\"], obj=project, strict=True)\n    assert \"django\" not in project.pyproject.dependency_groups[\"serve\"]\n"
  },
  {
    "path": "tests/cli/test_run.py",
    "content": "import json\nimport os\nimport subprocess\nimport textwrap\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\n\nimport pytest\n\nfrom pdm import termui\nfrom pdm.cli import actions\nfrom pdm.cli.utils import get_pep582_path\nfrom pdm.utils import cd\n\n\n@pytest.fixture\ndef _args(project):\n    (project.root / \"args.py\").write_text(\n        textwrap.dedent(\n            \"\"\"\n            import os\n            import sys\n            name = sys.argv[1]\n            args = \", \".join(sys.argv[2:])\n            print(f\"{name} CALLED with {args}\" if args else f\"{name} CALLED\")\n            \"\"\"\n        )\n    )\n\n\ndef test_pep582_launcher_for_python_interpreter(project, local_finder, pdm):\n    project.root.joinpath(\"main.py\").write_text(\"import first;print(first.first([0, False, 1, 2]))\\n\")\n    result = pdm([\"add\", \"first\"], obj=project)\n    assert result.exit_code == 0, result.stderr\n    env = os.environ.copy()\n    env.update({\"PYTHONPATH\": get_pep582_path(project)})\n    output = subprocess.check_output(\n        [str(project.python.executable), str(project.root.joinpath(\"main.py\"))],\n        env=env,\n    )\n    assert output.decode().strip() == \"1\"\n\n\ndef test_auto_isolate_site_packages(project, pdm):\n    env = os.environ.copy()\n    env.update({\"PYTHONPATH\": get_pep582_path(project)})\n    proc = subprocess.run(\n        [str(project.python.executable), \"-c\", \"import sys;print(sys.path, sep='\\\\n')\"],\n        env=env,\n        capture_output=True,\n        text=True,\n        cwd=str(project.root),\n        check=True,\n    )\n    assert any(\"site-packages\" in path for path in proc.stdout.splitlines())\n\n    result = pdm(\n        [\"run\", \"python\", \"-c\", \"import sys;print(sys.path, sep='\\\\n')\"],\n        obj=project,\n        strict=True,\n    )\n    assert not any(\"site-packages\" in path for path in result.stdout.splitlines())\n\n\ndef test_run_with_site_packages(project, pdm):\n    project.pyproject.settings[\"scripts\"] = {\n        \"foo\": {\n            \"cmd\": [\"python\", \"-c\", \"import sys;print(sys.path, sep='\\\\n')\"],\n            \"site_packages\": True,\n        }\n    }\n    project.pyproject.write()\n    result = pdm(\n        [\n            \"run\",\n            \"--site-packages\",\n            \"python\",\n            \"-c\",\n            \"import sys;print(sys.path, sep='\\\\n')\",\n        ],\n        obj=project,\n    )\n    assert result.exit_code == 0\n    result = pdm([\"run\", \"foo\"], obj=project)\n    assert result.exit_code == 0\n\n\ndef test_run_command_not_found(pdm):\n    result = pdm([\"run\", \"foobar\"])\n    assert \"Command 'foobar' is not found in your PATH.\" in result.stderr\n    assert result.exit_code == 1\n\n\ndef test_run_pass_exit_code(pdm):\n    result = pdm([\"run\", \"python\", \"-c\", \"1/0\"])\n    assert result.exit_code == 1\n\n\ndef test_run_cmd_script(project, pdm):\n    project.pyproject.settings[\"scripts\"] = {\"test_script\": \"python -V\"}\n    project.pyproject.write()\n    result = pdm([\"run\", \"test_script\"], obj=project)\n    assert result.exit_code == 0\n\n\ndef test_run_cmd_script_with_array(project, pdm):\n    project.pyproject.settings[\"scripts\"] = {\"test_script\": [\"python\", \"-c\", \"import sys; sys.exit(22)\"]}\n    project.pyproject.write()\n    result = pdm([\"run\", \"test_script\"], obj=project)\n    assert result.exit_code == 22\n\n\ndef test_run_script_pass_project_root(project, pdm, capfd):\n    project.pyproject.settings[\"scripts\"] = {\n        \"test_script\": [\n            \"python\",\n            \"-c\",\n            \"import os;print(os.getenv('PDM_PROJECT_ROOT'))\",\n        ]\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    result = pdm([\"run\", \"test_script\"], obj=project)\n    assert result.exit_code == 0\n    out, _ = capfd.readouterr()\n    assert Path(out.strip()) == project.root\n\n\ndef test_run_shell_script(project, pdm):\n    project.pyproject.settings[\"scripts\"] = {\n        \"test_script\": {\n            \"shell\": \"echo hello > output.txt\",\n            \"help\": \"test it won't fail\",\n        }\n    }\n    project.pyproject.write()\n    with cd(project.root):\n        result = pdm([\"run\", \"test_script\"], obj=project)\n    assert result.exit_code == 0\n    assert (project.root / \"output.txt\").read_text().strip() == \"hello\"\n\n\ndef test_run_script_with_relative_path(project, pdm, capfd):\n    if os.name == \"nt\":\n        (project.root / \"test_script.bat\").write_text(\"@echo Hello\\n\")\n    else:\n        (project.root / \"test_script.sh\").write_text(\"#!/bin/bash\\necho Hello\\n\")\n        (project.root / \"test_script.sh\").chmod(0o755)\n    with cd(project.root):\n        pdm([\"run\", \"./test_script.bat\" if os.name == \"nt\" else \"./test_script.sh\"], obj=project, strict=True)\n    out, _ = capfd.readouterr()\n    assert out.strip() == \"Hello\"\n\n\ndef test_run_non_existing_local_script(project, pdm):\n    with cd(project.root):\n        result = pdm([\"run\", \"./test_script.sh\"], obj=project)\n    assert result.exit_code != 0\n    assert \"not a valid executable\" in result.stderr\n\n\n@pytest.mark.parametrize(\n    \"args,expected\",\n    (\n        pytest.param([\"hello\"], \"ok hello\", id=\"with-args\"),\n        pytest.param([], \"ok\", id=\"without-args\"),\n    ),\n)\ndef test_run_shell_script_with_args_placeholder(project, pdm, args, expected):\n    project.pyproject.settings[\"scripts\"] = {\n        \"test_script\": {\n            \"shell\": \"echo ok {args} > output.txt\",\n            \"help\": \"test it won't fail\",\n        }\n    }\n    project.pyproject.write()\n    with cd(project.root):\n        result = pdm([\"run\", \"test_script\", *args], obj=project)\n    assert result.exit_code == 0\n    assert (project.root / \"output.txt\").read_text().strip() == expected\n\n\n@pytest.mark.parametrize(\n    \"args,expected\",\n    (\n        pytest.param([\"hello\"], \"hello\", id=\"with-args\"),\n        pytest.param([], \"default\", id=\"with-default\"),\n    ),\n)\ndef test_run_shell_script_with_args_placeholder_with_default(project, pdm, args, expected):\n    project.pyproject.settings[\"scripts\"] = {\n        \"test_script\": {\n            \"shell\": \"echo {args:default} > output.txt\",\n            \"help\": \"test it won't fail\",\n        }\n    }\n    project.pyproject.write()\n    with cd(project.root):\n        result = pdm([\"run\", \"test_script\", *args], obj=project)\n    assert result.exit_code == 0\n    assert (project.root / \"output.txt\").read_text().strip() == expected\n\n\ndef test_run_call_script(project, pdm):\n    (project.root / \"test_script.py\").write_text(\n        textwrap.dedent(\n            \"\"\"\n            import argparse\n            import sys\n\n            def main(argv=None):\n                parser = argparse.ArgumentParser()\n                parser.add_argument(\"-c\", \"--code\", type=int)\n                args = parser.parse_args(argv)\n                sys.exit(args.code)\n            \"\"\"\n        )\n    )\n    project.pyproject.settings[\"scripts\"] = {\n        \"test_script\": {\"call\": \"test_script:main\"},\n        \"test_script_with_args\": {\"call\": \"test_script:main(['-c', '9'])\"},\n    }\n    project.pyproject.write()\n    with cd(project.root):\n        result = pdm([\"run\", \"test_script\", \"-c\", \"8\"], obj=project)\n        assert result.exit_code == 8\n\n        result = pdm([\"run\", \"test_script_with_args\"], obj=project)\n        assert result.exit_code == 9\n\n\ndef test_run_script_with_extra_args(project, pdm, capfd):\n    (project.root / \"test_script.py\").write_text(\n        textwrap.dedent(\n            \"\"\"\n            import sys\n            print(*sys.argv[1:], sep='\\\\n')\n            \"\"\"\n        )\n    )\n    project.pyproject.settings[\"scripts\"] = {\"test_script\": \"python test_script.py\"}\n    project.pyproject.write()\n    with cd(project.root):\n        pdm([\"run\", \"test_script\", \"-a\", \"-b\", \"-c\"], obj=project)\n    out, _ = capfd.readouterr()\n    assert out.splitlines()[-3:] == [\"-a\", \"-b\", \"-c\"]\n\n\n@pytest.mark.parametrize(\n    \"args,expected\",\n    (\n        pytest.param([\"-a\", \"-b\", \"-c\"], [\"-a\", \"-b\", \"-c\", \"-x\"], id=\"with-args\"),\n        pytest.param([], [\"-x\"], id=\"without-args\"),\n    ),\n)\n@pytest.mark.parametrize(\n    \"script\",\n    (\n        pytest.param(\"python test_script.py {args} -x\", id=\"as-str\"),\n        pytest.param([\"python\", \"test_script.py\", \"{args}\", \"-x\"], id=\"as-list\"),\n    ),\n)\ndef test_run_script_with_args_placeholder(project, pdm, capfd, script, args, expected):\n    (project.root / \"test_script.py\").write_text(\n        textwrap.dedent(\n            \"\"\"\n            import sys\n            print(*sys.argv[1:], sep='\\\\n')\n            \"\"\"\n        )\n    )\n    project.pyproject.settings[\"scripts\"] = {\"test_script\": script}\n    project.pyproject.write()\n    with cd(project.root):\n        pdm([\"run\", \"-v\", \"test_script\", *args], obj=project)\n    out, _ = capfd.readouterr()\n    assert out.strip().splitlines()[1:] == expected\n\n\n@pytest.mark.parametrize(\n    \"args,expected\",\n    (\n        pytest.param([\"-a\", \"-b\", \"-c\"], [\"-a\", \"-b\", \"-c\", \"-x\"], id=\"with-args\"),\n        pytest.param([], [\"--default\", \"--value\", \"-x\"], id=\"default\"),\n    ),\n)\n@pytest.mark.parametrize(\n    \"script\",\n    (\n        pytest.param(\"python test_script.py {args:--default --value} -x\", id=\"as-str\"),\n        pytest.param([\"python\", \"test_script.py\", \"{args:--default --value}\", \"-x\"], id=\"as-list\"),\n    ),\n)\ndef test_run_script_with_args_placeholder_with_default(project, pdm, capfd, script, args, expected):\n    (project.root / \"test_script.py\").write_text(\n        textwrap.dedent(\n            \"\"\"\n            import sys\n            print(*sys.argv[1:], sep='\\\\n')\n            \"\"\"\n        )\n    )\n    project.pyproject.settings[\"scripts\"] = {\"test_script\": script}\n    project.pyproject.write()\n    with cd(project.root):\n        pdm([\"run\", \"-v\", \"test_script\", *args], obj=project)\n    out, _ = capfd.readouterr()\n    assert out.strip().splitlines()[1:] == expected\n\n\ndef test_run_shell_script_with_pdm_placeholder(project, pdm):\n    project.pyproject.settings[\"scripts\"] = {\n        \"test_script\": {\n            \"shell\": \"{pdm} -V > output.txt\",\n            \"help\": \"test it won't fail\",\n        }\n    }\n    project.pyproject.write()\n    with cd(project.root):\n        result = pdm([\"run\", \"test_script\"], obj=project)\n    assert result.exit_code == 0\n    assert (project.root / \"output.txt\").read_text().strip().startswith(\"PDM, version\")\n\n\ndef test_run_expand_env_vars(project, pdm, capfd, monkeypatch):\n    (project.root / \"test_script.py\").write_text(\"import os; print(os.getenv('FOO'))\")\n    project.pyproject.settings[\"scripts\"] = {\n        \"test_cmd\": 'python -c \"foo, bar = 0, 1;print(${FOO})\"',\n        \"test_cmd_no_expand\": \"python -c 'print(${FOO})'\",\n        \"test_script\": \"python test_script.py\",\n        \"test_cmd_array\": [\"python\", \"test_script.py\"],\n        \"test_shell\": {\"shell\": \"echo %FOO%\" if os.name == \"nt\" else \"echo $FOO\"},\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    with cd(project.root):\n        monkeypatch.setenv(\"FOO\", \"bar\")\n        pdm([\"run\", \"test_cmd\"], obj=project)\n        assert capfd.readouterr()[0].strip() == \"1\"\n\n        result = pdm([\"run\", \"test_cmd_no_expand\"], obj=project)\n        assert result.exit_code == 1\n\n        pdm([\"run\", \"test_script\"], obj=project)\n        assert capfd.readouterr()[0].strip() == \"bar\"\n\n        pdm([\"run\", \"test_cmd_array\"], obj=project)\n        assert capfd.readouterr()[0].strip() == \"bar\"\n\n        pdm([\"run\", \"test_shell\"], obj=project)\n        assert capfd.readouterr()[0].strip() == \"bar\"\n\n\ndef test_run_expand_env_vars_from_config(project, pdm, capfd):\n    (project.root / \"test_script.py\").write_text(\"import os; print(os.getenv('FOO'))\")\n    project.pyproject.settings[\"scripts\"] = {\n        \"test_cmd\": 'python -c \"foo, bar = 0, 1;print(${FOO})\"',\n        \"test_cmd_no_expand\": \"python -c 'print(${FOO})'\",\n        \"test_script\": \"python test_script.py\",\n        \"test_cmd_array\": [\"python\", \"test_script.py\"],\n        \"test_shell\": {\"shell\": \"echo %FOO%\" if os.name == \"nt\" else \"echo $FOO\"},\n        \"_\": {\"env\": {\"FOO\": \"bar\"}},\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    with cd(project.root):\n        pdm([\"run\", \"test_cmd\"], obj=project)\n        assert capfd.readouterr()[0].strip() == \"1\"\n\n        result = pdm([\"run\", \"test_cmd_no_expand\"], obj=project)\n        assert result.exit_code == 1\n\n        pdm([\"run\", \"test_script\"], obj=project)\n        assert capfd.readouterr()[0].strip() == \"bar\"\n\n        pdm([\"run\", \"test_cmd_array\"], obj=project)\n        assert capfd.readouterr()[0].strip() == \"bar\"\n\n        pdm([\"run\", \"test_shell\"], obj=project)\n        assert capfd.readouterr()[0].strip() == \"bar\"\n\n\ndef test_run_script_with_env_defined(project, pdm, capfd):\n    (project.root / \"test_script.py\").write_text(\"import os; print(os.getenv('FOO'))\")\n    project.pyproject.settings[\"scripts\"] = {\"test_script\": {\"cmd\": \"python test_script.py\", \"env\": {\"FOO\": \"bar\"}}}\n    project.pyproject.write()\n    capfd.readouterr()\n    with cd(project.root):\n        pdm([\"run\", \"test_script\"], obj=project)\n        assert capfd.readouterr()[0].strip() == \"bar\"\n\n\ndef test_run_script_with_dotenv_file(project, pdm, capfd, monkeypatch):\n    (project.root / \"test_script.py\").write_text(\"import os; print(os.getenv('FOO'), os.getenv('BAR'))\")\n    project.pyproject.settings[\"scripts\"] = {\n        \"test_override\": {\n            \"cmd\": \"python test_script.py\",\n            \"env_file\": {\"override\": \".env\"},\n        },\n        \"test_default\": {\"cmd\": \"python test_script.py\", \"env_file\": \".env\"},\n    }\n    project.pyproject.write()\n    monkeypatch.setenv(\"BAR\", \"foo\")\n    (project.root / \".env\").write_text(\"FOO=bar\\nBAR=override\")\n    capfd.readouterr()\n    with cd(project.root):\n        pdm([\"run\", \"test_default\"], obj=project)\n        assert capfd.readouterr()[0].strip() == \"bar foo\"\n        pdm([\"run\", \"test_override\"], obj=project)\n        assert capfd.readouterr()[0].strip() == \"bar override\"\n\n\ndef test_run_script_override_global_env(project, pdm, capfd):\n    (project.root / \"test_script.py\").write_text(\"import os; print(os.getenv('FOO'))\")\n    project.pyproject.settings[\"scripts\"] = {\n        \"_\": {\"env\": {\"FOO\": \"bar\"}},\n        \"test_env\": {\"cmd\": \"python test_script.py\"},\n        \"test_env_override\": {\"cmd\": \"python test_script.py\", \"env\": {\"FOO\": \"foobar\"}},\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    with cd(project.root):\n        pdm([\"run\", \"test_env\"], obj=project)\n        assert capfd.readouterr()[0].strip() == \"bar\"\n        pdm([\"run\", \"test_env_override\"], obj=project)\n        assert capfd.readouterr()[0].strip() == \"foobar\"\n\n\ndef test_run_show_list_of_scripts(project, pdm):\n    project.pyproject.settings[\"scripts\"] = {\n        \"test_composite\": {\"composite\": [\"test_cmd\", \"test_script\", \"test_shell\"]},\n        \"test_cmd\": \"flask db upgrade\",\n        \"test_multi\": \"\"\"\\\n            I am a multilines\n            command\n        \"\"\",\n        \"test_script\": {\"call\": \"test_script:main\", \"help\": \"call a python function\"},\n        \"test_shell\": {\"shell\": \"echo $FOO\", \"help\": \"shell command\"},\n    }\n    project.pyproject.write()\n    result = pdm([\"run\", \"--list\"], obj=project)\n    result_lines = result.output.splitlines()[3:]\n    assert result_lines[0][1:-1].strip() == \"test_cmd       │ cmd       │ flask db upgrade\"\n    sep = termui.Emoji.ARROW_SEPARATOR\n    assert result_lines[1][1:-1].strip() == f\"test_composite │ composite │ test_cmd {sep} test_script {sep} test_shell\"\n    assert result_lines[2][1:-1].strip() == f\"test_multi     │ cmd       │ I am a multilines{termui.Emoji.ELLIPSIS}\"\n    assert result_lines[3][1:-1].strip() == \"test_script    │ call      │ call a python function\"\n    assert result_lines[4][1:-1].strip() == \"test_shell     │ shell     │ shell command\"\n\n\ndef test_run_show_list_of_scripts_hide_internals(project, pdm):\n    project.pyproject.settings[\"scripts\"] = {\n        \"public\": \"true\",\n        \"_internal\": \"true\",\n    }\n    project.pyproject.write()\n    result = pdm([\"run\", \"--list\"], obj=project)\n    assert \"public\" in result.output\n    assert \"_internal\" not in result.output\n\n\ndef test_run_json_list_of_scripts(project, pdm):\n    project.pyproject.settings[\"scripts\"] = {\n        \"_\": {\"env_file\": \".env\"},\n        \"test_composite\": {\"composite\": [\"test_cmd\", \"test_script\", \"test_shell\"]},\n        \"test_cmd\": \"flask db upgrade\",\n        \"test_multi\": \"\"\"\\\n            I am a multilines\n            command\n        \"\"\",\n        \"test_script\": {\"call\": \"test_script:main\", \"help\": \"call a python function\"},\n        \"test_shell\": {\"shell\": \"echo $FOO\", \"help\": \"shell command\"},\n        \"test_env\": {\"cmd\": \"true\", \"env\": {\"TEST\": \"value\"}},\n        \"test_env_file\": {\"cmd\": \"true\", \"env_file\": \".env\"},\n        \"test_override\": {\"cmd\": \"true\", \"env_file\": {\"override\": \".env\"}},\n        \"test_site_packages\": {\"cmd\": \"true\", \"site_packages\": True},\n        \"_private\": \"true\",\n    }\n    project.pyproject.write()\n    result = pdm([\"run\", \"--json\"], obj=project, strict=True)\n\n    sep = termui.Emoji.ARROW_SEPARATOR\n    assert json.loads(result.outputs) == {\n        \"_\": {\"name\": \"_\", \"help\": \"Shared options\", \"kind\": \"shared\", \"env_file\": \".env\"},\n        \"test_cmd\": {\"name\": \"test_cmd\", \"help\": \"flask db upgrade\", \"kind\": \"cmd\", \"args\": \"flask db upgrade\"},\n        \"test_composite\": {\n            \"name\": \"test_composite\",\n            \"help\": f\"test_cmd {sep} test_script {sep} test_shell\",\n            \"kind\": \"composite\",\n            \"args\": [\"test_cmd\", \"test_script\", \"test_shell\"],\n        },\n        \"test_multi\": {\n            \"name\": \"test_multi\",\n            \"help\": f\"I am a multilines{termui.Emoji.ELLIPSIS}\",\n            \"kind\": \"cmd\",\n            \"args\": \"            I am a multilines\\n            command\\n        \",\n        },\n        \"test_script\": {\n            \"name\": \"test_script\",\n            \"help\": \"call a python function\",\n            \"kind\": \"call\",\n            \"args\": \"test_script:main\",\n        },\n        \"test_shell\": {\"name\": \"test_shell\", \"help\": \"shell command\", \"kind\": \"shell\", \"args\": \"echo $FOO\"},\n        \"test_env\": {\"name\": \"test_env\", \"help\": \"true\", \"kind\": \"cmd\", \"args\": \"true\", \"env\": {\"TEST\": \"value\"}},\n        \"test_env_file\": {\"name\": \"test_env_file\", \"help\": \"true\", \"kind\": \"cmd\", \"args\": \"true\", \"env_file\": \".env\"},\n        \"test_override\": {\n            \"name\": \"test_override\",\n            \"help\": \"true\",\n            \"kind\": \"cmd\",\n            \"args\": \"true\",\n            \"env_file.override\": \".env\",\n        },\n        \"test_site_packages\": {\n            \"name\": \"test_site_packages\",\n            \"help\": \"true\",\n            \"kind\": \"cmd\",\n            \"args\": \"true\",\n            \"site_packages\": True,\n        },\n        \"_private\": {\n            \"name\": \"_private\",\n            \"help\": \"true\",\n            \"kind\": \"cmd\",\n            \"args\": \"true\",\n        },\n    }\n\n\n@pytest.mark.usefixtures(\"local_finder\")\n@pytest.mark.parametrize(\"explicit_python\", [True, False])\ndef test_run_with_another_project_root(project, pdm, capfd, explicit_python):\n    project.pyproject.metadata[\"requires-python\"] = \">=3.6\"\n    project.pyproject.write()\n    pdm([\"add\", \"first\"], obj=project)\n    with TemporaryDirectory(prefix=\"pytest-run-\") as tmp_dir:\n        Path(tmp_dir).joinpath(\"main.py\").write_text(\"import first;print(first.first([0, False, 1, 2]))\\n\")\n        capfd.readouterr()\n        with cd(tmp_dir):\n            args = [\"run\", \"-p\", str(project.root), \"main.py\"]\n            if explicit_python:\n                args.insert(len(args) - 1, \"python\")\n            ret = pdm(args)\n            out, err = capfd.readouterr()\n            assert ret.exit_code == 0, err\n            assert out.strip() == \"1\"\n\n\ndef test_import_another_sitecustomize(project, pdm, capfd):\n    project.pyproject.metadata[\"requires-python\"] = \">=2.7\"\n    project.pyproject.write()\n    # a script for checking another sitecustomize is imported\n    project.root.joinpath(\"foo.py\").write_text(\"import os;print(os.getenv('FOO'))\")\n    # ensure there have at least one sitecustomize can be imported\n    # there may have more than one sitecustomize.py in sys.path\n    project.root.joinpath(\"sitecustomize.py\").write_text(\"import os;os.environ['FOO'] = 'foo'\")\n    env = os.environ.copy()\n    paths = env.get(\"PYTHONPATH\")\n    this_path = str(project.root)\n    new_paths = [this_path] if not paths else [this_path, paths]\n    env[\"PYTHONPATH\"] = os.pathsep.join(new_paths)\n    project._environment = None\n    capfd.readouterr()\n    with cd(project.root):\n        result = pdm([\"run\", \"python\", \"foo.py\"], env=env)\n    assert result.exit_code == 0, result.stderr\n    out, _ = capfd.readouterr()\n    assert out.strip() == \"foo\"\n\n\ndef test_run_with_patched_sysconfig(project, pdm, capfd):\n    project.root.joinpath(\"script.py\").write_text(\n        \"\"\"\\\nimport sysconfig\nimport json\nprint(json.dumps(sysconfig.get_paths()))\n\"\"\"\n    )\n    capfd.readouterr()\n    with cd(project.root):\n        result = pdm([\"run\", \"python\", \"script.py\"], obj=project)\n    assert result.exit_code == 0\n    out = json.loads(capfd.readouterr()[0])\n    assert \"__pypackages__\" in out[\"purelib\"]\n\n\ndef test_run_composite(project, pdm, capfd, _echo):\n    project.pyproject.settings[\"scripts\"] = {\n        \"first\": \"python echo.py First\",\n        \"second\": \"python echo.py Second\",\n        \"test\": {\"composite\": [\"first\", \"second\"]},\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", \"test\"], strict=True, obj=project)\n    out, _ = capfd.readouterr()\n    assert \"First CALLED\" in out\n    assert \"Second CALLED\" in out\n\n\ndef test_composite_stops_on_first_failure(project, pdm, capfd):\n    project.pyproject.settings[\"scripts\"] = {\n        \"first\": {\"cmd\": [\"python\", \"-c\", \"print('First CALLED')\"]},\n        \"fail\": \"python -c 'raise Exception'\",\n        \"second\": \"echo 'Second CALLED'\",\n        \"test\": {\"composite\": [\"first\", \"fail\", \"second\"]},\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    result = pdm([\"run\", \"test\"], obj=project)\n    assert result.exit_code == 1\n    out, _ = capfd.readouterr()\n    assert \"First CALLED\" in out\n    assert \"Second CALLED\" not in out\n\n\ndef test_composite_keep_going_on_failure(project, pdm, capfd):\n    project.pyproject.settings[\"scripts\"] = {\n        \"first\": {\"cmd\": [\"python\", \"-c\", \"print('First CALLED')\"]},\n        \"fail\": \"python -c 'raise Exception'\",\n        \"second\": \"echo 'Second CALLED'\",\n        \"test\": {\"composite\": [\"first\", \"fail\", \"second\"], \"keep_going\": True},\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    result = pdm([\"run\", \"test\"], obj=project)\n    assert result.exit_code == 1\n    out, _ = capfd.readouterr()\n    assert \"First CALLED\" in out\n    assert \"Second CALLED\" in out\n\n\ndef test_composite_inherit_env(project, pdm, capfd, _echo):\n    project.pyproject.settings[\"scripts\"] = {\n        \"first\": {\n            \"cmd\": \"python echo.py First VAR\",\n            \"env\": {\"VAR\": \"42\"},\n        },\n        \"second\": {\n            \"cmd\": \"python echo.py Second VAR\",\n            \"env\": {\"VAR\": \"42\"},\n        },\n        \"nested\": {\n            \"composite\": [\"third\"],\n            \"env\": {\"VAR\": \"42\"},\n        },\n        \"third\": {\n            \"cmd\": \"python echo.py Third VAR\",\n            \"env\": {\"VAR\": \"42\"},\n        },\n        \"test\": {\"composite\": [\"first\", \"second\", \"nested\"], \"env\": {\"VAR\": \"overridden\"}},\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", \"test\"], strict=True, obj=project)\n    out, _ = capfd.readouterr()\n    assert \"First CALLED with VAR=overridden\" in out\n    assert \"Second CALLED with VAR=overridden\" in out\n    assert \"Third CALLED with VAR=overridden\" in out\n\n\ndef test_composite_fail_on_first_missing_task(project, pdm, capfd, _echo):\n    project.pyproject.settings[\"scripts\"] = {\n        \"first\": \"python echo.py First\",\n        \"second\": \"python echo.py Second\",\n        \"test\": {\"composite\": [\"first\", \"fail\", \"second\"]},\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    result = pdm([\"run\", \"test\"], obj=project)\n    assert result.exit_code == 1\n    out, _ = capfd.readouterr()\n    assert \"First CALLED\" in out\n    assert \"Second CALLED\" not in out\n\n\ndef test_composite_fails_on_recursive_script(project, pdm):\n    project.pyproject.settings[\"scripts\"] = {\n        \"first\": {\"composite\": [\"first\"]},\n        \"second\": {\"composite\": [\"third\"]},\n        \"third\": {\"composite\": [\"second\"]},\n        \"fourth\": {\"composite\": [\"python -V\", \"python -V\"]},\n        \"fifth\": {\"composite\": [\"fourth\", \"fourth\"]},\n    }\n    project.pyproject.write()\n    result = pdm([\"run\", \"first\"], obj=project)\n    assert result.exit_code == 1\n    assert \"Script first is recursive\" in result.stderr\n\n    result = pdm([\"run\", \"second\"], obj=project)\n    assert result.exit_code == 1\n    assert \"Script second is recursive\" in result.stderr\n\n    result = pdm([\"run\", \"fourth\"], obj=project)\n    assert result.exit_code == 0\n\n    result = pdm([\"run\", \"fifth\"], obj=project)\n    assert result.exit_code == 0\n\n\ndef test_composite_runs_all_hooks(project, pdm, capfd, _echo):\n    project.pyproject.settings[\"scripts\"] = {\n        \"test\": {\"composite\": [\"first\", \"second\"]},\n        \"pre_test\": \"python echo.py Pre-Test\",\n        \"post_test\": \"python echo.py Post-Test\",\n        \"first\": \"python echo.py First\",\n        \"pre_first\": \"python echo.py Pre-First\",\n        \"second\": \"python echo.py Second\",\n        \"post_second\": \"python echo.py Post-Second\",\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", \"test\"], strict=True, obj=project)\n    out, _ = capfd.readouterr()\n    assert \"Pre-Test CALLED\" in out\n    assert \"Pre-First CALLED\" in out\n    assert \"First CALLED\" in out\n    assert \"Second CALLED\" in out\n    assert \"Post-Second CALLED\" in out\n    assert \"Post-Test CALLED\" in out\n\n\ndef test_composite_pass_parameters_to_subtasks(project, pdm, capfd, _args):\n    project.pyproject.settings[\"scripts\"] = {\n        \"test\": {\"composite\": [\"first\", \"second\"]},\n        \"pre_test\": \"python args.py Pre-Test\",\n        \"post_test\": \"python args.py Post-Test\",\n        \"first\": \"python args.py First\",\n        \"pre_first\": \"python args.py Pre-First\",\n        \"second\": \"python args.py Second\",\n        \"post_second\": \"python args.py Post-Second\",\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", \"test\", \"param=value\"], strict=True, obj=project)\n    out, _ = capfd.readouterr()\n    assert \"Pre-Test CALLED\" in out\n    assert \"Pre-First CALLED\" in out\n    assert \"First CALLED with param=value\" in out\n    assert \"Second CALLED with param=value\" in out\n    assert \"Post-Second CALLED\" in out\n    assert \"Post-Test CALLED\" in out\n\n\ndef test_composite_can_pass_parameters(project, pdm, capfd, _args):\n    project.pyproject.settings[\"scripts\"] = {\n        \"test\": {\"composite\": [\"first param=first\", \"second param=second\"]},\n        \"pre_test\": \"python args.py Pre-Test\",\n        \"post_test\": \"python args.py Post-Test\",\n        \"first\": \"python args.py First\",\n        \"pre_first\": \"python args.py Pre-First\",\n        \"second\": \"python args.py Second\",\n        \"post_second\": \"python args.py Post-Second\",\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", \"test\"], strict=True, obj=project)\n    out, _ = capfd.readouterr()\n    assert \"Pre-Test CALLED\" in out\n    assert \"Pre-First CALLED\" in out\n    assert \"First CALLED with param=first\" in out\n    assert \"Second CALLED with param=second\" in out\n    assert \"Post-Second CALLED\" in out\n    assert \"Post-Test CALLED\" in out\n\n\n@pytest.mark.parametrize(\n    \"args,expected\",\n    (\n        pytest.param([\"-a\"], \"-a, \", id=\"with-args\"),\n        pytest.param([], \"\", id=\"without-args\"),\n    ),\n)\ndef test_composite_only_pass_parameters_to_subtasks_with_args(project, pdm, capfd, _args, args, expected):\n    project.pyproject.settings[\"scripts\"] = {\n        \"test\": {\"composite\": [\"first\", \"second {args} key=value\"]},\n        \"first\": \"python args.py First\",\n        \"second\": \"python args.py Second\",\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", \"-v\", \"test\", *args], strict=True, obj=project)\n    out, _ = capfd.readouterr()\n    assert \"First CALLED\" in out\n    assert f\"Second CALLED with {expected}key=value\" in out\n\n\n@pytest.mark.parametrize(\n    \"args,expected\",\n    (\n        pytest.param([\"-a\"], \"-a\", id=\"with-args\"),\n        pytest.param([], \"--default\", id=\"default\"),\n    ),\n)\ndef test_composite_only_pass_parameters_to_subtasks_with_args_with_default(project, pdm, capfd, _args, args, expected):\n    project.pyproject.settings[\"scripts\"] = {\n        \"test\": {\"composite\": [\"first\", \"second {args:--default} key=value\"]},\n        \"first\": \"python args.py First\",\n        \"second\": \"python args.py Second\",\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", \"-v\", \"test\", *args], strict=True, obj=project)\n    out, _ = capfd.readouterr()\n    assert \"First CALLED\" in out\n    assert f\"Second CALLED with {expected}, key=value\" in out\n\n\ndef test_composite_hooks_inherit_env(project, pdm, capfd, _echo):\n    project.pyproject.settings[\"scripts\"] = {\n        \"pre_task\": {\"cmd\": \"python echo.py Pre-Task VAR\", \"env\": {\"VAR\": \"42\"}},\n        \"task\": \"python echo.py Task\",\n        \"post_task\": {\"cmd\": \"python echo.py Post-Task VAR\", \"env\": {\"VAR\": \"42\"}},\n        \"test\": {\"composite\": [\"task\"], \"env\": {\"VAR\": \"overridden\"}},\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", \"test\"], strict=True, obj=project)\n    out, _ = capfd.readouterr()\n    assert \"Pre-Task CALLED with VAR=overridden\" in out\n    assert \"Task CALLED\" in out\n    assert \"Post-Task CALLED with VAR=overridden\" in out\n\n\ndef test_composite_inherit_env_in_cascade(project, pdm, capfd, _echo):\n    project.pyproject.settings[\"scripts\"] = {\n        \"_\": {\"env\": {\"FOO\": \"BAR\", \"TIK\": \"TOK\"}},\n        \"pre_task\": {\n            \"cmd\": \"python echo.py Pre-Task VAR FOO TIK\",\n            \"env\": {\"VAR\": \"42\", \"FOO\": \"foobar\"},\n        },\n        \"task\": {\n            \"cmd\": \"python echo.py Task VAR FOO TIK\",\n            \"env\": {\"VAR\": \"42\", \"FOO\": \"foobar\"},\n        },\n        \"post_task\": {\n            \"cmd\": \"python echo.py Post-Task VAR FOO TIK\",\n            \"env\": {\"VAR\": \"42\", \"FOO\": \"foobar\"},\n        },\n        \"test\": {\"composite\": [\"task\"], \"env\": {\"VAR\": \"overridden\"}},\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", \"test\"], strict=True, obj=project)\n    out, _ = capfd.readouterr()\n    assert \"Pre-Task CALLED with VAR=overridden FOO=foobar TIK=TOK\" in out\n    assert \"Task CALLED with VAR=overridden FOO=foobar TIK=TOK\" in out\n    assert \"Post-Task CALLED with VAR=overridden FOO=foobar TIK=TOK\" in out\n\n\ndef test_composite_inherit_dotfile(project, pdm, capfd, _echo):\n    (project.root / \".env\").write_text(\"VAR=42\")\n    (project.root / \"override.env\").write_text(\"VAR=overridden\")\n    project.pyproject.settings[\"scripts\"] = {\n        \"pre_task\": {\"cmd\": \"python echo.py Pre-Task VAR\", \"env_file\": \".env\"},\n        \"task\": {\"cmd\": \"python echo.py Task VAR\", \"env_file\": \".env\"},\n        \"post_task\": {\"cmd\": \"python echo.py Post-Task VAR\", \"env_file\": \".env\"},\n        \"test\": {\"composite\": [\"task\"], \"env_file\": \"override.env\"},\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", \"test\"], strict=True, obj=project)\n    out, _ = capfd.readouterr()\n    assert \"Pre-Task CALLED with VAR=overridden\" in out\n    assert \"Task CALLED with VAR=overridden\" in out\n    assert \"Post-Task CALLED with VAR=overridden\" in out\n\n\ndef test_resolve_env_vars_in_dotfile(project, pdm, capfd, _echo):\n    (project.root / \".env\").write_text(\"VAR=42\\nFOO=${OUT}/${VAR}\")\n    project.pyproject.settings[\"scripts\"] = {\n        \"_\": {\"env_file\": \".env\"},\n        \"test\": {\"cmd\": \"python echo.py Task FOO BAR\", \"env\": {\"BAR\": \"${FOO}/bar\"}},\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", \"test\"], strict=True, obj=project, env={\"OUT\": \"hello\"})\n    out, _ = capfd.readouterr()\n    assert \"Task CALLED with FOO=hello/42 BAR=hello/42/bar\" in out\n\n\ndef test_composite_can_have_commands(project, pdm, capfd):\n    project.pyproject.settings[\"scripts\"] = {\n        \"task\": {\"cmd\": [\"python\", \"-c\", 'print(\"Task CALLED\")']},\n        \"test\": {\"composite\": [\"task\", \"python -c 'print(\\\"Command CALLED\\\")'\"]},\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", \"-v\", \"test\"], strict=True, obj=project)\n    out, _ = capfd.readouterr()\n    assert \"Task CALLED\" in out\n    assert \"Command CALLED\" in out\n\n\ndef test_run_shortcut(project, pdm, capfd):\n    project.pyproject.settings[\"scripts\"] = {\n        \"test\": \"echo 'Everything is fine'\",\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    result = pdm([\"test\"], obj=project, strict=True)\n    assert result.exit_code == 0\n    out, _ = capfd.readouterr()\n    assert \"Everything is fine\" in out\n\n\ndef test_run_shortcuts_dont_override_commands(project, pdm, capfd, mocker):\n    do_lock = mocker.patch.object(actions, \"do_lock\")\n    do_sync = mocker.patch.object(actions, \"do_sync\")\n    project.pyproject.settings[\"scripts\"] = {\n        \"install\": \"echo 'Should not run'\",\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    result = pdm([\"install\"], obj=project, strict=True)\n    assert result.exit_code == 0\n    out, _ = capfd.readouterr()\n    assert \"Should not run\" not in out\n    do_lock.assert_called_once()\n    do_sync.assert_called_once()\n\n\ndef test_run_shortcut_fail_with_usage_if_script_not_found(project, pdm):\n    result = pdm([\"whatever\"], obj=project)\n    assert result.exit_code != 0\n    assert \"Command not found: whatever\" in result.stderr\n    assert \"usage\" in result.stderr.lower()\n\n\n@pytest.mark.parametrize(\n    \"args\",\n    [\n        pytest.param([\"-ko\"], id=\"unknown param\"),\n        pytest.param([\"pip\", \"--version\"], id=\"not an user script\"),\n    ],\n)\ndef test_empty_positional_args_still_display_usage(project, pdm, args):\n    result = pdm(args, obj=project)\n    assert result.exit_code != 0\n    assert \"usage\" in result.stderr.lower()\n\n\ndef test_empty_positional_args_display_help(project, pdm):\n    result = pdm([], obj=project)\n    assert result.exit_code == 0\n    assert \"usage:\" in result.output.lower()\n    assert \"commands:\" in result.output.lower()\n    assert \"options:\" in result.output.lower()\n\n\ndef test_run_script_changing_working_dir(project, pdm, capfd):\n    project.root.joinpath(\"subdir\").mkdir()\n    project.root.joinpath(\"subdir\", \"file.text\").write_text(\"Hello world\\n\")\n    project.pyproject.settings[\"scripts\"] = {\n        \"test_script\": {\"working_dir\": \"subdir\", \"cmd\": \"cat file.text\"},\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", \"test_script\"], obj=project, strict=True)\n    assert capfd.readouterr()[0].strip() == \"Hello world\"\n\n\ndef test_run_script_with_inline_metadata(project, pdm, local_finder, local_finder_artifacts):\n    with cd(project.root):\n        project.root.joinpath(\"test_script.py\").write_text(\n            textwrap.dedent(\"\"\"\\\n            from first import first\n\n            assert first([0, False, 1, 2]) == 1\n            \"\"\")\n        )\n        result = pdm([\"run\", \"test_script.py\"], obj=project)\n        assert result.exit_code != 0\n\n    local_artifacts_url = local_finder_artifacts.as_uri()\n\n    project.root.joinpath(\"test_script.py\").write_text(\n        textwrap.dedent(f\"\"\"\\\n        # /// script\n        # requires-python = \">=3.9\"\n        # dependencies = [\n        #   \"first\",\n        # ]\n        #\n        # [[tool.pdm.source]]\n        # name = \"pypi\"\n        # url = \"{local_artifacts_url}\"\n        # type = \"find_links\"\n        # ///\n        from first import first\n\n        assert first([0, False, 1, 2]) == 1\n        \"\"\")\n    )\n    with cd(project.root):\n        result = pdm([\"run\", \"test_script.py\"], obj=project)\n        assert result.exit_code == 0\n\n\ndef test_run_script_pass_run_cwd(project, pdm, capfd):\n    project.pyproject.settings[\"scripts\"] = {\n        \"test_script\": [\n            \"python\",\n            \"-c\",\n            \"import os;print(os.getenv('PDM_RUN_CWD'))\",\n        ]\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    result = pdm([\"run\", \"test_script\"], obj=project)\n    assert result.exit_code == 0\n    out, _ = capfd.readouterr()\n    assert Path(out.strip()) == Path.cwd()\n\n\ndef test_run_script_pass_run_cwd_to_original_working_dir_when_working_dir_of_script_is_changed(project, pdm, capfd):\n    project.root.joinpath(\"subdir\").mkdir()\n    project.root.joinpath(\"subdir\", \"test_script.py\").write_text(\n        textwrap.dedent(\"\"\"\\\n           import os\n           print(os.getenv('PDM_RUN_CWD', ''))\n        \"\"\")\n    )\n    project.pyproject.settings[\"scripts\"] = {\n        \"test_script\": {\"working_dir\": \"subdir\", \"cmd\": \"python test_script.py\"},\n    }\n    project.pyproject.write()\n    capfd.readouterr()\n    pdm([\"run\", \"test_script\"], obj=project, strict=True)\n    assert capfd.readouterr()[0].strip() == str(Path.cwd())\n\n\ndef test_run_script_default_verbosity(project, pdm):\n    project.pyproject.settings[\"scripts\"] = {\"test\": {\"cmd\": \"python -V\", \"help\": \"help\"}}\n    project.pyproject.write()\n\n    result = pdm([\"run\", \"test\"], strict=True, obj=project)\n\n    assert \"task test\" not in result.stderr\n    assert \"['python', '-V']\" not in result.stderr\n    assert \"help\" not in result.stderr\n\n\ndef test_run_script_default_verbosity_with_show_header(project, pdm):\n    project.project_config.update({\"scripts.show_header\": True})\n    project.pyproject.settings[\"scripts\"] = {\"test\": {\"cmd\": \"python -V\", \"help\": \"help\"}}\n    project.pyproject.write()\n\n    result = pdm([\"run\", \"test\"], strict=True, obj=project)\n\n    assert \"task test\" in result.stderr\n    assert \"['python', '-V']\" not in result.stderr\n    assert \"python -V\" not in result.stderr\n    assert \"help\" in result.stderr\n\n\ndef test_run_script_default_verbosity_with_show_header_no_help(project, pdm):\n    project.project_config.update({\"scripts.show_header\": True})\n    project.pyproject.settings[\"scripts\"] = {\"test\": {\"cmd\": \"python -V\"}}\n    project.pyproject.write()\n\n    result = pdm([\"run\", \"test\"], strict=True, obj=project)\n\n    assert \"task test\" in result.stderr\n    assert \"python -V\" in result.stderr\n    assert \"['python', '-V']\" not in result.stderr\n\n\ndef test_run_script_verbose(project, pdm):\n    project.pyproject.settings[\"scripts\"] = {\"test\": {\"cmd\": \"python -V\", \"help\": \"help\"}}\n    project.pyproject.write()\n\n    result = pdm([\"run\", \"-v\", \"test\"], strict=True, obj=project)\n\n    assert \"task test\" in result.stderr\n    assert \"['python', '-V']\" in result.stderr\n    assert \"python -V\" not in result.stderr\n    assert \"help\" not in result.stderr\n\n\ndef test_run_composite_script_default_verbosity_with_show_header(project, pdm):\n    project.project_config.update({\"scripts.show_header\": True})\n    project.pyproject.settings[\"scripts\"] = {\n        \"test\": {\"cmd\": \"python -V\", \"help\": \"help\"},\n        \"parent\": {\"composite\": [\"test\", \"test\"]},\n    }\n    project.pyproject.write()\n\n    result = pdm([\"run\", \"parent\"], strict=True, obj=project)\n\n    assert \"task parent\" in result.stderr\n    assert f\"test {termui.Emoji.ARROW_SEPARATOR} test\" in result.stderr\n    assert \"['test', 'test']\" not in result.stderr\n    assert \"task test\" in result.stderr\n    assert \"python -V\" not in result.stderr\n    assert \"['python', '-V']\" not in result.stderr\n    assert \"help\" in result.stderr\n\n\ndef test_run_composite_script_default_verbosity_with_show_header_and_help(project, pdm):\n    project.project_config.update({\"scripts.show_header\": True})\n    project.pyproject.settings[\"scripts\"] = {\n        \"test\": {\"cmd\": \"python -V\", \"help\": \"help\"},\n        \"parent\": {\"composite\": [\"test\", \"test\"], \"help\": \"composite\"},\n    }\n    project.pyproject.write()\n\n    result = pdm([\"run\", \"parent\"], strict=True, obj=project)\n\n    assert \"task parent\" in result.stderr\n    assert \"composite\" in result.stderr\n    assert f\"test {termui.Emoji.ARROW_SEPARATOR} test\" not in result.stderr\n    assert \"['test', 'test']\" not in result.stderr\n    assert \"task test\" in result.stderr\n    assert \"['python', '-V']\" not in result.stderr\n    assert \"python -V\" not in result.stderr\n    assert \"help\" in result.stderr\n\n\ndef test_run_composite_script_verbose(project, pdm):\n    project.pyproject.settings[\"scripts\"] = {\n        \"test\": {\"cmd\": \"python -V\", \"help\": \"help\"},\n        \"parent\": {\"composite\": [\"test\", \"test\"], \"help\": \"composite\"},\n    }\n    project.pyproject.write()\n\n    result = pdm([\"run\", \"-v\", \"parent\"], strict=True, obj=project)\n\n    assert \"task parent\" in result.stderr\n    assert \"composite\" not in result.stderr\n    assert f\"test {termui.Emoji.ARROW_SEPARATOR} test\" not in result.stderr\n    assert \"['test', 'test']\" in result.stderr\n    assert \"task test\" in result.stderr\n    assert \"['python', '-V']\" in result.stderr\n    assert \"python -V\" not in result.stderr\n    assert \"help\" not in result.stderr\n"
  },
  {
    "path": "tests/cli/test_search.py",
    "content": "\"\"\"Tests for the search command utilities\"\"\"\n\nfrom pdm.cli.commands.search import print_results\n\n\ndef test_print_results_empty_hits(mocker):\n    \"\"\"Test print_results with empty hits returns early\"\"\"\n    ui = mocker.Mock()\n    working_set = mocker.Mock()\n\n    # Should return early without calling echo\n    print_results(ui, [], working_set)\n\n    ui.echo.assert_not_called()\n\n\ndef test_print_results_with_hits(mocker):\n    \"\"\"Test print_results with search hits\"\"\"\n    ui = mocker.Mock()\n    working_set = {}\n\n    # Create mock search hits\n    hit1 = mocker.Mock()\n    hit1.name = \"test-package\"\n    hit1.version = \"1.0.0\"\n    hit1.summary = \"A test package\"\n\n    hit2 = mocker.Mock()\n    hit2.name = \"another-package\"\n    hit2.version = \"2.0.0\"\n    hit2.summary = \"Another test package\"\n\n    hits = [hit1, hit2]\n\n    print_results(ui, hits, working_set)\n\n    # Should call echo for each hit\n    assert ui.echo.call_count >= 2\n\n\ndef test_print_results_with_installed_package(mocker):\n    \"\"\"Test print_results shows INSTALLED for packages in working set\"\"\"\n    ui = mocker.Mock()\n    working_set = mocker.Mock()\n\n    # Mock a package that's installed\n    hit = mocker.Mock()\n    hit.name = \"installed-package\"\n    hit.version = \"1.0.0\"\n    hit.summary = \"An installed package\"\n\n    # Mock working set to return a distribution\n    dist = mocker.Mock()\n    dist.version = \"1.0.0\"\n    working_set.__contains__ = mocker.Mock(return_value=True)\n    working_set.__getitem__ = mocker.Mock(return_value=dist)\n\n    print_results(ui, [hit], working_set)\n\n    # Should show INSTALLED label\n    calls = [str(call) for call in ui.echo.call_args_list]\n    assert any(\"INSTALLED\" in str(call) for call in calls)\n\n\ndef test_print_results_with_terminal_width(mocker):\n    \"\"\"Test print_results respects terminal width for wrapping\"\"\"\n    ui = mocker.Mock()\n    working_set = {}\n\n    hit = mocker.Mock()\n    hit.name = \"test-package\"\n    hit.version = \"1.0.0\"\n    hit.summary = \"This is a very long summary that should be wrapped when terminal width is specified\"\n\n    print_results(ui, [hit], working_set, terminal_width=40)\n\n    # Should call echo\n    ui.echo.assert_called()\n\n\ndef test_print_results_unicode_error(mocker):\n    \"\"\"Test print_results handles UnicodeEncodeError gracefully\"\"\"\n    ui = mocker.Mock()\n    working_set = {}\n\n    hit = mocker.Mock()\n    hit.name = \"test-package\"\n    hit.version = \"1.0.0\"\n    hit.summary = \"Test summary\"\n\n    # Make echo raise UnicodeEncodeError\n    ui.echo.side_effect = UnicodeEncodeError(\"utf-8\", \"\", 0, 1, \"test\")\n\n    # Should not raise exception\n    print_results(ui, [hit], working_set)\n\n\ndef test_search_command_deprecation_warning(pdm):\n    \"\"\"Test that search command shows deprecation warning\"\"\"\n    result = pdm([\"search\", \"test\"])\n    # Command should succeed but show warning\n    assert result.exit_code == 0\n    assert \"deprecated\" in result.stderr.lower() or \"deprecated\" in result.output.lower()\n"
  },
  {
    "path": "tests/cli/test_self_command.py",
    "content": "from types import SimpleNamespace\nfrom unittest.mock import ANY\n\nimport pytest\n\nfrom pdm.cli.commands import self_cmd\n\n\ndef mock_distribution(metadata, entry_points=()):\n    entry_points = (SimpleNamespace(group=ep) for ep in entry_points)\n    return SimpleNamespace(metadata=metadata, entry_points=entry_points)\n\n\nDISTRIBUTIONS = {\n    \"foo\": mock_distribution({\"Name\": \"foo\", \"Version\": \"1.0.0\", \"Summary\": \"Foo package\"}, [\"pdm.plugin\"]),\n    \"bar\": mock_distribution({\"Name\": \"bar\", \"Version\": \"2.0.0\", \"Summary\": \"Bar package\"}, [\"pdm\"]),\n    \"baz\": mock_distribution({\"Name\": \"baz\", \"Version\": \"3.0.0\", \"Summary\": \"Baz package\"}),\n}\n\n\n@pytest.fixture()\ndef mock_pip(mocker):\n    mocked = mocker.patch(\"pdm.cli.commands.self_cmd.run_pip\")\n    return mocked\n\n\n@pytest.fixture()\ndef mock_all_distributions(mocker):\n    mocker.patch(\"pdm.cli.commands.self_cmd.WorkingSet\", return_value=DISTRIBUTIONS)\n\n\n@pytest.fixture()\ndef mock_latest_pdm_version(mocker):\n    return mocker.patch(\n        \"pdm.cli.commands.self_cmd.get_latest_pdm_version_from_pypi\",\n    )\n\n\n@pytest.mark.usefixtures(\"mock_all_distributions\")\ndef test_self_list(pdm):\n    result = pdm([\"self\", \"list\"])\n    assert result.exit_code == 0, result.stderr\n    packages = [line.split()[0] for line in result.stdout.splitlines()]\n    assert packages == [\"bar\", \"baz\", \"foo\"]\n\n\n@pytest.mark.usefixtures(\"mock_all_distributions\")\ndef test_self_list_plugins(pdm):\n    result = pdm([\"self\", \"list\", \"--plugins\"])\n    assert result.exit_code == 0, result.stderr\n    packages = [line.split()[0] for line in result.stdout.splitlines()]\n    assert packages == [\"bar\", \"foo\"]\n\n\ndef test_self_add(pdm, mock_pip):\n    result = pdm([\"self\", \"add\", \"foo\"])\n    assert result.exit_code == 0, result.stderr\n    mock_pip.assert_called_with(ANY, [\"install\", \"foo\"])\n\n    result = pdm([\"self\", \"add\", \"--pip-args\", \"--force-reinstall --upgrade\", \"foo\"])\n    assert result.exit_code == 0, result.stderr\n    mock_pip.assert_called_with(ANY, [\"install\", \"--force-reinstall\", \"--upgrade\", \"foo\"])\n\n\ndef test_self_remove(pdm, mock_pip, mocker, monkeypatch):\n    from rich import get_console\n\n    console = get_console()\n\n    def _mock_resolve(packages):\n        return [\"demo\", \"pytz\"] if \"demo\" in packages else packages\n\n    mocker.patch.object(\n        self_cmd.RemoveCommand,\n        \"_resolve_dependencies_to_remove\",\n        side_effect=_mock_resolve,\n    )\n    mocker.patch.object(console, \"is_interactive\", True)\n\n    result = pdm([\"self\", \"remove\", \"foo\"])\n    assert result.exit_code != 0\n\n    result = pdm([\"self\", \"remove\", \"-y\", \"demo\"])\n    assert result.exit_code == 0, result.stderr\n    mock_pip.assert_called_with(ANY, [\"uninstall\", \"-y\", \"demo\", \"pytz\"])\n\n    with monkeypatch.context() as m:\n        m.setenv(\"PDM_NON_INTERACTIVE\", \"1\")\n        result = pdm([\"self\", \"remove\", \"demo\"])\n        assert result.exit_code == 0, result.stderr\n        mock_pip.assert_called_with(ANY, [\"uninstall\", \"-y\", \"demo\", \"pytz\"])\n\n    result = pdm([\"-n\", \"self\", \"remove\", \"demo\"])\n    assert result.exit_code == 0, result.stderr\n    mock_pip.assert_called_with(ANY, [\"uninstall\", \"-y\", \"demo\", \"pytz\"])\n\n\n@pytest.mark.parametrize(\n    \"args,expected\",\n    [\n        ([\"self\", \"update\"], [\"install\", \"--upgrade\", \"--upgrade-strategy\", \"eager\", \"pdm[locked]==99.0.0\"]),\n        ([\"self\", \"update\", \"--pre\"], [\"install\", \"--upgrade\", \"--upgrade-strategy\", \"eager\", \"pdm[locked]==99.0.1b1\"]),\n        (\n            [\"self\", \"update\", \"--head\"],\n            [\"install\", \"--upgrade\", \"--upgrade-strategy\", \"eager\", f\"pdm[locked] @ git+{self_cmd.PDM_REPO}@main\"],\n        ),\n    ],\n)\ndef test_self_update(pdm, mock_pip, mock_latest_pdm_version, args, expected):\n    def mocked_latest_version(project, pre):\n        return \"99.0.1b1\" if pre else \"99.0.0\"\n\n    mock_latest_pdm_version.side_effect = mocked_latest_version\n\n    result = pdm(args)\n    assert result.exit_code == 0, result.stderr\n    mock_pip.assert_called_with(ANY, expected)\n\n\ndef test_self_update_already_latest(pdm, mock_pip, mock_latest_pdm_version):\n    mock_latest_pdm_version.return_value = \"0.0.0\"\n\n    result = pdm([\"self\", \"update\"])\n    assert result.exit_code == 0, result.stderr\n    assert \"Already up-to-date\" in result.stdout\n    mock_pip.assert_not_called()\n"
  },
  {
    "path": "tests/cli/test_show.py",
    "content": "\"\"\"Additional tests for the show command\"\"\"\n\nimport pytest\n\nfrom pdm.cli.commands.show import filter_stable\n\n\ndef test_filter_stable_with_stable_version(mocker):\n    \"\"\"Test filter_stable returns True for stable versions\"\"\"\n    # Mock package with stable version\n    package = mocker.Mock()\n    package.version = \"1.0.0\"\n    assert filter_stable(package) is True\n\n\ndef test_filter_stable_with_prerelease_alpha(mocker):\n    \"\"\"Test filter_stable returns False for alpha prereleases\"\"\"\n    package = mocker.Mock()\n    package.version = \"1.0.0a1\"\n    assert filter_stable(package) is False\n\n\ndef test_filter_stable_with_prerelease_beta(mocker):\n    \"\"\"Test filter_stable returns False for beta prereleases\"\"\"\n    package = mocker.Mock()\n    package.version = \"2.0.0b2\"\n    assert filter_stable(package) is False\n\n\ndef test_filter_stable_with_prerelease_rc(mocker):\n    \"\"\"Test filter_stable returns False for release candidates\"\"\"\n    package = mocker.Mock()\n    package.version = \"3.0.0rc1\"\n    assert filter_stable(package) is False\n\n\ndef test_filter_stable_with_dev_version(mocker):\n    \"\"\"Test filter_stable returns False for dev versions\"\"\"\n    package = mocker.Mock()\n    package.version = \"1.0.0.dev1\"\n    assert filter_stable(package) is False\n\n\n@pytest.mark.network\ndef test_show_command_with_specific_metadata_keys(pdm):\n    \"\"\"Test show command with specific metadata keys\"\"\"\n    result = pdm([\"show\", \"requests\", \"--name\"])\n    assert result.exit_code == 0\n    assert \"requests\" in result.output.lower()\n\n    result = pdm([\"show\", \"requests\", \"--version\"])\n    assert result.exit_code == 0\n    # Should contain a version number\n\n\n@pytest.mark.network\ndef test_show_command_with_multiple_metadata_keys(pdm):\n    \"\"\"Test show command with multiple metadata keys only shows selected ones\"\"\"\n    result = pdm([\"show\", \"requests\", \"--name\", \"--version\"])\n    assert result.exit_code == 0\n    # Should only show name and version, not full metadata\n\n\ndef test_show_command_non_distribution_project(project, pdm):\n    \"\"\"Test show command on a non-distribution project raises error\"\"\"\n    # Mark the project as non-distribution\n    project.pyproject.settings[\"distribution\"] = False\n\n    result = pdm([\"show\"], obj=project)\n    assert result.exit_code != 0\n    assert \"not a library\" in result.stderr\n"
  },
  {
    "path": "tests/cli/test_template.py",
    "content": "import os\n\nimport pytest\n\nfrom pdm.cli.templates import ProjectTemplate\nfrom pdm.exceptions import PdmException\n\n\ndef test_non_pyproject_template_disallowed(project_no_init):\n    with ProjectTemplate(\"tests/fixtures/projects/demo_extras\") as template:\n        with pytest.raises(PdmException, match=r\"Template pyproject.toml not found\"):\n            template.generate(project_no_init.root, {\"project\": {\"name\": \"foo\"}})\n\n\ndef test_module_project_template(project_no_init):\n    metadata = {\n        \"project\": {\"name\": \"foo\", \"version\": \"0.1.0\", \"requires-python\": \">=3.10\"},\n        \"build-system\": {\"requires\": [\"pdm-backend\"], \"build-backend\": \"pdm.backend\"},\n    }\n\n    with ProjectTemplate(\"tests/fixtures/projects/demo\") as template:\n        template.generate(project_no_init.root, metadata)\n\n    project_no_init.pyproject.reload()\n    assert project_no_init.pyproject.metadata[\"name\"] == \"foo\"\n    assert project_no_init.pyproject.metadata[\"requires-python\"] == \">=3.10\"\n    assert project_no_init.pyproject._data[\"build-system\"] == metadata[\"build-system\"]\n    assert project_no_init.pyproject.metadata[\"dependencies\"] == [\"idna\", \"chardet; os_name=='nt'\"]\n    assert project_no_init.pyproject.metadata[\"optional-dependencies\"][\"tests\"] == [\"pytest\"]\n    assert (project_no_init.root / \"foo.py\").exists()\n    assert os.access(project_no_init.root / \"foo.py\", os.W_OK)\n\n\ndef test_module_project_template_generate_application(project_no_init):\n    metadata = {\n        \"project\": {\"name\": \"\", \"version\": \"\", \"requires-python\": \">=3.10\"},\n    }\n\n    with ProjectTemplate(\"tests/fixtures/projects/demo\") as template:\n        template.generate(project_no_init.root, metadata)\n\n    project_no_init.pyproject.reload()\n    assert project_no_init.pyproject.metadata[\"name\"] == \"\"\n    assert \"build-system\" not in project_no_init.pyproject._data\n    assert project_no_init.pyproject.metadata[\"dependencies\"] == [\"idna\", \"chardet; os_name=='nt'\"]\n    assert (project_no_init.root / \"demo.py\").exists()\n\n\ndef test_package_project_template(project_no_init):\n    metadata = {\n        \"project\": {\"name\": \"foo\", \"version\": \"0.1.0\", \"requires-python\": \">=3.10\"},\n        \"build-system\": {\"requires\": [\"pdm-backend\"], \"build-backend\": \"pdm.backend\"},\n    }\n\n    with ProjectTemplate(\"tests/fixtures/projects/demo-package\") as template:\n        template.generate(project_no_init.root, metadata)\n\n    project_no_init.pyproject.reload()\n    assert project_no_init.pyproject.metadata[\"name\"] == \"foo\"\n    assert project_no_init.pyproject.metadata[\"requires-python\"] == \">=3.10\"\n    assert project_no_init.pyproject._data[\"build-system\"] == metadata[\"build-system\"]\n    assert (project_no_init.root / \"foo\").is_dir()\n    assert (project_no_init.root / \"foo/__init__.py\").exists()\n    assert project_no_init.pyproject.settings[\"version\"] == {\"path\": \"foo/__init__.py\", \"source\": \"file\"}\n"
  },
  {
    "path": "tests/cli/test_update.py",
    "content": "import pytest\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_update_packages_with_top(project, pdm):\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    result = pdm([\"update\", \"--top\", \"requests\"], obj=project)\n    assert \"PdmUsageError\" in result.stderr\n\n\ndef test_update_command(project, pdm, mocker):\n    do_update = mocker.patch(\"pdm.cli.commands.update.Command.do_update\")\n    pdm([\"update\"], obj=project)\n    do_update.assert_called_once()\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_update_ignore_constraints(project, repository, pdm):\n    project.project_config[\"strategy.save\"] = \"compatible\"\n    pdm([\"add\", \"pytz\"], obj=project, strict=True)\n    assert project.pyproject.metadata[\"dependencies\"] == [\"pytz~=2019.3\"]\n    repository.add_candidate(\"pytz\", \"2020.2\")\n\n    pdm([\"update\", \"pytz\"], obj=project, strict=True)\n    assert project.pyproject.metadata[\"dependencies\"] == [\"pytz~=2019.3\"]\n    assert project.get_locked_repository().candidates[\"pytz\"].version == \"2019.3\"\n\n    pdm([\"update\", \"pytz\", \"--unconstrained\"], obj=project, strict=True)\n    assert project.pyproject.metadata[\"dependencies\"] == [\"pytz~=2020.2\"]\n    assert project.get_locked_repository().candidates[\"pytz\"].version == \"2020.2\"\n\n    pdm([\"add\", \"chardet\"], obj=project, strict=True)\n    assert \"chardet~=3.0\" in project.pyproject.metadata[\"dependencies\"]\n    assert project.get_locked_repository().candidates[\"chardet\"].version == \"3.0.4\"\n    repository.add_candidate(\"chardet\", \"3.0.6\")\n\n    pdm([\"update\", \"chardet\", \"--unconstrained\", \"--save-safe-compatible\"], obj=project, strict=True)\n    assert \"chardet~=3.0.6\" in project.pyproject.metadata[\"dependencies\"]\n\n\n@pytest.mark.usefixtures(\"working_set\")\n@pytest.mark.parametrize(\"strategy\", [\"reuse\", \"all\"])\ndef test_update_all_packages(project, repository, pdm, strategy):\n    pdm([\"add\", \"requests\", \"pytz\"], obj=project, strict=True)\n    repository.add_candidate(\"pytz\", \"2019.6\")\n    repository.add_candidate(\"chardet\", \"3.0.5\")\n    repository.add_candidate(\"requests\", \"2.20.0\")\n    repository.add_dependencies(\n        \"requests\",\n        \"2.20.0\",\n        [\n            \"certifi>=2017.4.17\",\n            \"chardet<3.1.0,>=3.0.2\",\n            \"idna<2.8,>=2.5\",\n            \"urllib3<1.24,>=1.21.1\",\n        ],\n    )\n    result = pdm([\"update\", f\"--update-{strategy}\"], obj=project, strict=True)\n    locked_candidates = project.get_locked_repository().candidates\n    assert locked_candidates[\"requests\"].version == \"2.20.0\"\n    assert locked_candidates[\"chardet\"].version == (\"3.0.5\" if strategy == \"all\" else \"3.0.4\")\n    assert locked_candidates[\"pytz\"].version == \"2019.6\"\n    update_num = 3 if strategy == \"all\" else 2\n    assert f\"{update_num} to update\" in result.stdout, result.stdout\n\n    result = pdm([\"sync\"], obj=project, strict=True)\n    assert \"All packages are synced to date\" in result.stdout\n\n\ndef test_update_no_lock(project, working_set, repository, pdm):\n    pdm([\"add\", \"pytz\"], obj=project, strict=True)\n    repository.add_candidate(\"pytz\", \"2019.6\")\n    pdm([\"update\", \"--frozen-lockfile\"], obj=project, strict=True)\n    assert working_set[\"pytz\"].version == \"2019.6\"\n    project.lockfile.reload()\n    assert project.get_locked_repository().candidates[\"pytz\"].version == \"2019.3\"\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_update_dry_run(project, repository, pdm):\n    pdm([\"add\", \"requests\", \"pytz\"], obj=project, strict=True)\n    repository.add_candidate(\"pytz\", \"2019.6\")\n    repository.add_candidate(\"chardet\", \"3.0.5\")\n    repository.add_candidate(\"requests\", \"2.20.0\")\n    repository.add_dependencies(\n        \"requests\",\n        \"2.20.0\",\n        [\n            \"certifi>=2017.4.17\",\n            \"chardet<3.1.0,>=3.0.2\",\n            \"idna<2.8,>=2.5\",\n            \"urllib3<1.24,>=1.21.1\",\n        ],\n    )\n    result = pdm([\"update\", \"--dry-run\"], obj=project, strict=True)\n    project.lockfile.reload()\n    locked_candidates = project.get_locked_repository().candidates\n    assert locked_candidates[\"requests\"].version == \"2.19.1\"\n    assert locked_candidates[\"chardet\"].version == \"3.0.4\"\n    assert locked_candidates[\"pytz\"].version == \"2019.3\"\n    assert \"requests 2.19.1 -> 2.20.0\" in result.stdout\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_update_top_packages_dry_run(project, repository, pdm):\n    pdm([\"add\", \"requests\", \"pytz\"], obj=project, strict=True)\n    repository.add_candidate(\"pytz\", \"2019.6\")\n    repository.add_candidate(\"chardet\", \"3.0.5\")\n    repository.add_candidate(\"requests\", \"2.20.0\")\n    repository.add_dependencies(\n        \"requests\",\n        \"2.20.0\",\n        [\n            \"certifi>=2017.4.17\",\n            \"chardet<3.1.0,>=3.0.2\",\n            \"idna<2.8,>=2.5\",\n            \"urllib3<1.24,>=1.21.1\",\n        ],\n    )\n    result = pdm([\"update\", \"--dry-run\", \"--top\"], obj=project, strict=True)\n    assert \"requests 2.19.1 -> 2.20.0\" in result.stdout\n    assert \"- chardet 3.0.4 -> 3.0.5\" not in result.stdout\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_update_specified_packages(project, repository, pdm):\n    pdm([\"add\", \"requests\", \"pytz\", \"--no-sync\"], obj=project, strict=True)\n    repository.add_candidate(\"pytz\", \"2019.6\")\n    repository.add_candidate(\"chardet\", \"3.0.5\")\n    repository.add_candidate(\"requests\", \"2.20.0\")\n    repository.add_dependencies(\n        \"requests\",\n        \"2.20.0\",\n        [\n            \"certifi>=2017.4.17\",\n            \"chardet<3.1.0,>=3.0.2\",\n            \"idna<2.8,>=2.5\",\n            \"urllib3<1.24,>=1.21.1\",\n        ],\n    )\n    pdm([\"update\", \"requests\"], obj=project, strict=True)\n    locked_candidates = project.get_locked_repository().candidates\n    assert locked_candidates[\"requests\"].version == \"2.20.0\"\n    assert locked_candidates[\"chardet\"].version == \"3.0.4\"\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_update_specified_packages_eager_mode(project, repository, pdm):\n    pdm([\"add\", \"requests\", \"pytz\", \"--no-sync\"], obj=project, strict=True)\n    repository.add_candidate(\"pytz\", \"2019.6\")\n    repository.add_candidate(\"chardet\", \"3.0.5\")\n    repository.add_candidate(\"requests\", \"2.20.0\")\n    repository.add_dependencies(\n        \"requests\",\n        \"2.20.0\",\n        [\n            \"certifi>=2017.4.17\",\n            \"chardet<3.1.0,>=3.0.2\",\n            \"idna<2.8,>=2.5\",\n            \"urllib3<1.24,>=1.21.1\",\n        ],\n    )\n    pdm([\"update\", \"requests\", \"--update-eager\"], obj=project, strict=True)\n    locked_candidates = project.get_locked_repository().candidates\n    assert locked_candidates[\"requests\"].version == \"2.20.0\"\n    assert locked_candidates[\"chardet\"].version == \"3.0.5\"\n    assert locked_candidates[\"pytz\"].version == \"2019.3\"\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_update_transitive(project, repository, pdm):\n    pdm([\"add\", \"requests\", \"--no-sync\"], obj=project, strict=True)\n    repository.add_candidate(\"chardet\", \"3.0.5\")\n    repository.add_candidate(\"requests\", \"2.20.0\")\n    repository.add_dependencies(\n        \"requests\",\n        \"2.20.0\",\n        [\n            \"certifi>=2017.4.17\",\n            \"chardet<3.1.0,>=3.0.2\",\n            \"idna<2.8,>=2.5\",\n            \"urllib3<1.24,>=1.21.1\",\n        ],\n    )\n    pdm([\"update\", \"chardet\"], obj=project, strict=True)\n    locked_candidates = project.get_locked_repository().candidates\n    assert not any(\"chardet\" in dependency for dependency in project.pyproject.metadata[\"dependencies\"])\n    assert locked_candidates[\"chardet\"].version == \"3.0.5\"\n    assert locked_candidates[\"requests\"].version == \"2.19.1\"\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_update_transitive_nonexistent_dependencies(project, pdm):\n    pdm([\"add\", \"requests\", \"--no-sync\"], obj=project, strict=True)\n    result = pdm([\"update\", \"numpy\"], obj=project)\n    assert \"ProjectError\" in result.stderr\n    assert \"numpy does not exist in\" in result.stderr\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_update_package_wrong_group(project, pdm):\n    pdm([\"add\", \"-d\", \"requests\"], obj=project, strict=True)\n    result = pdm([\"update\", \"requests\"], obj=project)\n    assert \"ProjectError\" in result.stderr\n    assert \"requests does not exist in default, but exists in dev\" in result.stderr\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_update_transitive_non_transitive_dependencies(project, repository, pdm):\n    pdm([\"add\", \"requests\", \"pytz\", \"--no-sync\"], obj=project, strict=True)\n    repository.add_candidate(\"pytz\", \"2019.6\")\n    repository.add_candidate(\"chardet\", \"3.0.5\")\n    repository.add_candidate(\"requests\", \"2.20.0\")\n    repository.add_dependencies(\n        \"requests\",\n        \"2.20.0\",\n        [\n            \"certifi>=2017.4.17\",\n            \"chardet<3.1.0,>=3.0.2\",\n            \"idna<2.8,>=2.5\",\n            \"urllib3<1.24,>=1.21.1\",\n        ],\n    )\n    pdm([\"update\", \"requests\", \"chardet\", \"pytz\"], obj=project, strict=True)\n    locked_candidates = project.get_locked_repository().candidates\n    assert not any(\"chardet\" in dependency for dependency in project.pyproject.metadata[\"dependencies\"])\n    assert locked_candidates[\"requests\"].version == \"2.20.0\"\n    assert locked_candidates[\"chardet\"].version == \"3.0.5\"\n    assert locked_candidates[\"pytz\"].version == \"2019.6\"\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_update_specified_packages_eager_mode_config(project, repository, pdm):\n    pdm([\"add\", \"requests\", \"pytz\", \"--no-sync\"], obj=project, strict=True)\n    repository.add_candidate(\"pytz\", \"2019.6\")\n    repository.add_candidate(\"chardet\", \"3.0.5\")\n    repository.add_candidate(\"requests\", \"2.20.0\")\n    repository.add_dependencies(\n        \"requests\",\n        \"2.20.0\",\n        [\n            \"certifi>=2017.4.17\",\n            \"chardet<3.1.0,>=3.0.2\",\n            \"idna<2.8,>=2.5\",\n            \"urllib3<1.24,>=1.21.1\",\n        ],\n    )\n    pdm([\"config\", \"strategy.update\", \"eager\"], obj=project, strict=True)\n    pdm([\"update\", \"requests\"], obj=project, strict=True)\n    locked_candidates = project.get_locked_repository().candidates\n    assert locked_candidates[\"requests\"].version == \"2.20.0\"\n    assert locked_candidates[\"chardet\"].version == \"3.0.5\"\n    assert locked_candidates[\"pytz\"].version == \"2019.3\"\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_update_with_package_and_groups_argument(project, pdm):\n    pdm([\"add\", \"-G\", \"web\", \"requests\"], obj=project, strict=True)\n    pdm([\"add\", \"-Gextra\", \"pytz\"], obj=project, strict=True)\n    result = pdm([\"update\", \"requests\", \"--group\", \"web\", \"-G\", \"extra\"], obj=project)\n    assert \"PdmUsageError\" in result.stderr\n\n    result = pdm([\"update\", \"requests\", \"--no-default\"], obj=project)\n    assert \"PdmUsageError\" in result.stderr\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_update_with_prerelease_without_package_argument(project, pdm):\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    result = pdm([\"update\", \"--prerelease\"], obj=project)\n    assert \"--prerelease/--stable must be used with packages given\" in result.stderr\n\n\ndef test_update_existing_package_with_prerelease(project, working_set, pdm):\n    project.project_config[\"strategy.save\"] = \"compatible\"\n    pdm([\"add\", \"urllib3\"], obj=project, strict=True)\n    assert project.pyproject.metadata[\"dependencies\"][0] == \"urllib3~=1.22\"\n    assert working_set[\"urllib3\"].version == \"1.22\"\n\n    pdm([\"update\", \"urllib3\", \"--prerelease\"], obj=project, strict=True)\n    assert project.pyproject.metadata[\"dependencies\"][0] == \"urllib3~=1.22\"\n    assert working_set[\"urllib3\"].version == \"1.23b0\"\n\n    pdm([\"update\", \"urllib3\"], obj=project, strict=True)  # prereleases should be kept\n    assert working_set[\"urllib3\"].version == \"1.23b0\"\n\n    pdm([\"update\", \"urllib3\", \"--stable\"], obj=project, strict=True)\n    assert working_set[\"urllib3\"].version == \"1.22\"\n\n    pdm([\"update\", \"urllib3\", \"--prerelease\", \"--unconstrained\"], obj=project, strict=True)\n    assert project.pyproject.metadata[\"dependencies\"][0] == \"urllib3<2,>=1.23b0\"\n    assert working_set[\"urllib3\"].version == \"1.23b0\"\n\n\ndef test_update_package_with_extras(project, repository, working_set, pdm):\n    repository.add_candidate(\"foo\", \"0.1\")\n    foo_deps = [\"urllib3; extra == 'req'\"]\n    repository.add_dependencies(\"foo\", \"0.1\", foo_deps)\n    pdm([\"add\", \"foo[req]\"], obj=project, strict=True)\n    assert working_set[\"foo\"].version == \"0.1\"\n\n    repository.add_candidate(\"foo\", \"0.2\")\n    repository.add_dependencies(\"foo\", \"0.2\", foo_deps)\n    pdm([\"update\"], obj=project, strict=True)\n    assert working_set[\"foo\"].version == \"0.2\"\n    assert project.get_locked_repository().candidates[\"foo\"].version == \"0.2\"\n\n\ndef test_update_groups_in_lockfile(project, working_set, pdm, repository):\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    repository.add_candidate(\"foo\", \"0.1\")\n    pdm([\"add\", \"foo\", \"--group\", \"extra\"], obj=project, strict=True)\n    assert project.lockfile.groups == [\"default\", \"extra\"]\n    repository.add_candidate(\"foo\", \"0.2\")\n    pdm([\"update\"], obj=project, strict=True)\n    assert project.get_locked_repository().candidates[\"foo\"].version == \"0.2\"\n    assert working_set[\"foo\"].version == \"0.2\"\n\n\ndef test_update_group_not_in_lockfile(project, working_set, pdm):\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    project.add_dependencies([\"pytz\"], to_group=\"extra\")\n    result = pdm([\"update\", \"--group\", \"extra\"], obj=project)\n    assert result.exit_code != 0\n    assert \"Requested groups not in lockfile: extra\" in result.stderr\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_update_dependency_group_with_include(project, pdm):\n    from pdm.formats.base import make_array\n\n    project.pyproject.dependency_groups.update({\"tz\": [\"pytz\"], \"web\": make_array([{\"include-group\": \"tz\"}])})\n    project.pyproject.write()\n    pdm([\"update\", \"-u\"], obj=project, strict=True)\n"
  },
  {
    "path": "tests/cli/test_use.py",
    "content": "import os\nimport shutil\nimport sys\nfrom pathlib import Path\n\nimport pytest\n\nfrom pdm.cli.commands.use import Command as UseCommand\nfrom pdm.exceptions import NoPythonVersion\nfrom pdm.models.caches import JSONFileCache\n\n\ndef test_use_command(project, pdm):\n    python = \"python\" if os.name == \"nt\" else \"python3\"\n    python_path = shutil.which(python)\n    result = pdm([\"use\", \"-f\", python], obj=project)\n    assert result.exit_code == 0\n    config_content = project.root.joinpath(\".pdm-python\").read_text()\n    assert Path(python_path).as_posix() in config_content\n\n    result = pdm([\"use\", \"-f\", python_path], obj=project)\n    assert result.exit_code == 0\n    project.pyproject.metadata[\"requires-python\"] = \">=3.6\"\n    result = pdm([\"use\", \"2.7\"], obj=project)\n    assert result.exit_code == 1\n\n\ndef test_use_python_by_version(project, pdm):\n    python_version = \".\".join(map(str, sys.version_info[:2]))\n    result = pdm([\"use\", \"-f\", python_version], obj=project)\n    assert result.exit_code == 0\n\n\n@pytest.mark.skipif(os.name != \"posix\", reason=\"Run on POSIX platforms only\")\ndef test_use_wrapper_python(project):\n    wrapper_script = f\"\"\"#!/bin/bash\nexec \"{sys.executable}\" \"$@\"\n\"\"\"\n    shim_path = project.root.joinpath(\"python_shim.sh\")\n    shim_path.write_text(wrapper_script)\n    shim_path.chmod(0o755)\n\n    UseCommand().do_use(project, shim_path.as_posix())\n    assert project.python.executable == Path(sys.executable)\n\n\n@pytest.mark.skipif(os.name != \"posix\", reason=\"Run on POSIX platforms only\")\ndef test_use_invalid_wrapper_python(project):\n    wrapper_script = \"\"\"#!/bin/bash\necho hello\n\"\"\"\n    shim_path = project.root.joinpath(\"python_shim.sh\")\n    shim_path.write_text(wrapper_script)\n    shim_path.chmod(0o755)\n    with pytest.raises(NoPythonVersion):\n        UseCommand().do_use(project, shim_path.as_posix())\n\n\ndef test_use_remember_last_selection(project, mocker):\n    (project.cache_dir / \"use_cache.json\").unlink(missing_ok=True)\n    cache = JSONFileCache(project.cache_dir / \"use_cache.json\")\n    do_use = UseCommand().do_use\n    do_use(project, first=True)\n    cache._read_cache()\n    assert not cache._cache\n    do_use(project, \"3\", first=True)\n    cache._read_cache()\n    assert \"3\" in cache\n    mocker.patch.object(project, \"find_interpreters\")\n    do_use(project, \"3\")\n    project.find_interpreters.assert_not_called()\n\n\ndef test_use_venv_python(project, pdm):\n    pdm([\"venv\", \"create\"], obj=project, strict=True)\n    pdm([\"venv\", \"create\", \"--name\", \"test\"], obj=project, strict=True)\n    project.global_config[\"python.use_venv\"] = True\n    venv_location = project.config[\"venv.location\"]\n    do_use = UseCommand().do_use\n    do_use(project, venv=\"in-project\")\n    assert project.python.executable.parent.parent == project.root.joinpath(\".venv\")\n    do_use(project, venv=\"test\")\n    assert project.python.executable.parent.parent.parent == Path(venv_location)\n    with pytest.raises(Exception, match=\"No virtualenv with key 'non-exists' is found\"):\n        do_use(project, venv=\"non-exists\")\n\n\ndef test_use_auto_install_and_no_auto_install_are_mutual_exclusive(project, pdm):\n    command = [\"use\", \"--auto-install-min\", \"-f\"]\n    with pytest.raises(RuntimeError) as error:\n        result = pdm(command, obj=project, strict=True)\n        assert str(error.value).startswith(f\"Call command {command} failed\")\n        assert result.exit_code != 0\n\n    command = [\"use\", \"--auto-install-max\", \"-f\"]\n    with pytest.raises(RuntimeError) as error:\n        result = pdm(command, obj=project, strict=True)\n        assert str(error.value).startswith(f\"Call command {command} failed\")\n        assert result.exit_code != 0\n\n    command = [\"use\", \"--auto-install-max\", \"--auto-install-min\"]\n    with pytest.raises(RuntimeError) as error:\n        result = pdm(command, obj=project, strict=True)\n        assert str(error.value).startswith(f\"Call command {command} failed\")\n        assert result.exit_code != 0\n"
  },
  {
    "path": "tests/cli/test_utils.py",
    "content": "def test_help_with_unknown_arguments(pdm):\n    result = pdm([\"add\", \"--unknown-args\"])\n    assert \"usage: pdm add \" in result.stderr.lower()\n    assert result.exit_code == 2\n\n\ndef test_output_similar_command_when_typo(pdm):\n    result = pdm([\"instal\"])\n    assert \"install\" in result.stderr\n    assert result.exit_code == 2\n"
  },
  {
    "path": "tests/cli/test_venv.py",
    "content": "import os\nimport platform\nimport re\nimport shutil\nimport sys\nfrom unittest.mock import ANY\n\nimport pytest\nimport shellingham\n\nfrom pdm.cli.commands.venv import backends\nfrom pdm.cli.commands.venv.utils import get_venv_prefix\n\n\n@pytest.fixture(params=[True, False])\ndef with_pip(request):\n    return request.param\n\n\n@pytest.fixture()\ndef fake_create(monkeypatch):\n    def fake_create(self, location, *args, prompt=None):\n        bin_dir = \"Scripts\" if sys.platform == \"win32\" else \"bin\"\n        suffix = \".exe\" if sys.platform == \"win32\" else \"\"\n        (location / bin_dir).mkdir(parents=True)\n        (location / bin_dir / f\"python{suffix}\").touch()\n\n    monkeypatch.setattr(backends.VirtualenvBackend, \"perform_create\", fake_create)\n    monkeypatch.setattr(backends.VenvBackend, \"perform_create\", fake_create)\n    monkeypatch.setattr(backends.CondaBackend, \"perform_create\", fake_create)\n\n\n@pytest.mark.usefixtures(\"fake_create\")\ndef test_venv_create(pdm, project):\n    project._saved_python = None\n    project.project_config[\"venv.in_project\"] = False\n    result = pdm([\"venv\", \"create\"], obj=project)\n    assert result.exit_code == 0, result.stderr\n    venv_path = re.match(r\"Virtualenv (.+) is created successfully\", result.output).group(1)\n    assert os.path.exists(venv_path)\n    assert not project._saved_python\n\n\n@pytest.mark.usefixtures(\"fake_create\")\ndef test_venv_create_in_project(pdm, project):\n    project.project_config[\"venv.in_project\"] = True\n    pdm([\"venv\", \"create\"], obj=project, strict=True)\n    venv_path = project.root / \".venv\"\n    assert venv_path.exists()\n    result = pdm([\"venv\", \"create\"], obj=project)\n    assert result.exit_code == 1\n    assert \"is not empty\" in result.stderr\n\n\n@pytest.mark.usefixtures(\"fake_create\")\ndef test_venv_create_other_location(pdm, project):\n    pdm([\"venv\", \"-p\", project.root.as_posix(), \"create\"], strict=True)\n    venv_path = project.root / \".venv\"\n    assert venv_path.exists()\n    result = pdm([\"venv\", \"-p\", project.root.as_posix(), \"create\"])\n    assert result.exit_code == 1\n    assert \"is not empty\" in result.stderr\n\n\n@pytest.mark.usefixtures(\"fake_create\")\ndef test_venv_show_path(pdm, project):\n    project.project_config[\"venv.in_project\"] = True\n    pdm([\"venv\", \"create\"], obj=project, strict=True)\n    pdm([\"venv\", \"create\", \"--name\", \"test\"], obj=project, strict=True)\n    result = pdm([\"venv\", \"--path\", \"in-project\"], obj=project, strict=True)\n    assert result.output.strip() == str(project.root / \".venv\")\n    result = pdm([\"venv\", \"--path\", \"test\"], obj=project)\n    assert result.exit_code == 0\n    result = pdm([\"venv\", \"--path\", \"foo\"], obj=project)\n    assert result.exit_code == 1\n\n\n@pytest.mark.usefixtures(\"fake_create\")\ndef test_venv_list(pdm, project):\n    project.project_config[\"venv.in_project\"] = False\n    result = pdm([\"venv\", \"create\"], obj=project)\n    assert result.exit_code == 0, result.stderr\n    venv_path = re.match(r\"Virtualenv (.+) is created successfully\", result.output).group(1)\n\n    result = pdm([\"venv\", \"list\"], obj=project)\n    assert result.exit_code == 0, result.stderr\n    assert venv_path in result.output\n\n\n@pytest.mark.usefixtures(\"fake_create\")\ndef test_venv_remove(pdm, project):\n    project.project_config[\"venv.in_project\"] = False\n    result = pdm([\"venv\", \"create\"], obj=project)\n    assert result.exit_code == 0, result.stderr\n    venv_path = re.match(r\"Virtualenv (.+) is created successfully\", result.output).group(1)\n    key = os.path.basename(venv_path)[len(get_venv_prefix(project)) :]\n\n    result = pdm([\"venv\", \"remove\", \"non-exist\"], obj=project)\n    assert result.exit_code != 0\n\n    result = pdm([\"venv\", \"remove\", \"-y\", key], obj=project)\n    assert result.exit_code == 0, result.stderr\n\n    assert not os.path.exists(venv_path)\n\n\n@pytest.mark.usefixtures(\"fake_create\")\ndef test_venv_recreate(pdm, project):\n    project.project_config[\"venv.in_project\"] = False\n    result = pdm([\"venv\", \"create\"], obj=project)\n    assert result.exit_code == 0, result.stderr\n\n    result = pdm([\"venv\", \"create\"], obj=project)\n    assert result.exit_code != 0\n\n    result = pdm([\"venv\", \"create\", \"-f\"], obj=project)\n    assert result.exit_code == 0, result.stderr\n\n\n@pytest.mark.usefixtures(\"venv_backends\")\ndef test_venv_activate(pdm, mocker, project):\n    project.project_config[\"venv.in_project\"] = False\n    result = pdm([\"venv\", \"create\"], obj=project)\n    assert result.exit_code == 0, result.stderr\n    venv_path = re.match(r\"Virtualenv (.+) is created successfully\", result.output).group(1)\n    key = os.path.basename(venv_path)[len(get_venv_prefix(project)) :]\n\n    mocker.patch(\"shellingham.detect_shell\", return_value=(\"bash\", None))\n    result = pdm([\"venv\", \"activate\", key], obj=project)\n    assert result.exit_code == 0, result.stderr\n    backend = project.config[\"venv.backend\"]\n\n    if backend == \"conda\":\n        assert result.output.startswith(\"conda activate\")\n    else:\n        assert result.output.strip(\"'\\\"\\n\").endswith(\"activate\")\n        if platform.system() == \"Windows\":\n            assert not result.output.startswith(\"source\")\n            assert not result.output.startswith(\"'\")\n        else:\n            assert result.output.startswith(\"source\")\n\n\n@pytest.mark.usefixtures(\"venv_backends\")\n@pytest.mark.skipif(platform.system() == \"Windows\", reason=\"UNIX only\")\ndef test_venv_activate_tcsh(pdm, mocker, project):\n    project.project_config[\"venv.in_project\"] = False\n    result = pdm([\"venv\", \"create\"], obj=project)\n    assert result.exit_code == 0, result.stderr\n    venv_path = re.match(r\"Virtualenv (.+) is created successfully\", result.output).group(1)\n    key = os.path.basename(venv_path)[len(get_venv_prefix(project)) :]\n\n    mocker.patch(\"shellingham.detect_shell\", return_value=(\"tcsh\", None))\n    result = pdm([\"venv\", \"activate\", key], obj=project)\n    assert result.output.startswith(\"source\") and result.output.strip(\"'\\\"\\n\").endswith(\"activate.csh\")\n\n\n@pytest.mark.usefixtures(\"venv_backends\")\ndef test_venv_activate_custom_prompt(pdm, mocker, project):\n    project.project_config[\"venv.in_project\"] = False\n    creator = mocker.patch(\"pdm.cli.commands.venv.backends.Backend.create\")\n    result = pdm([\"venv\", \"create\"], obj=project)\n    assert result.exit_code == 0, result.stderr\n    creator.assert_called_once_with(\n        None, [], False, False, prompt=project.project_config[\"venv.prompt\"], with_pip=False\n    )\n\n\ndef test_venv_activate_project_without_python(pdm, project):\n    project._saved_python = None\n    result = pdm([\"venv\", \"activate\"], obj=project)\n    assert result.exit_code != 0\n    assert \"The project doesn't have a saved python.path\" in result.stderr\n\n\n@pytest.mark.usefixtures(\"fake_create\")\ndef test_venv_activate_error(pdm, project):\n    project.project_config[\"venv.in_project\"] = False\n    result = pdm([\"venv\", \"create\"], obj=project, strict=True)\n\n    result = pdm([\"venv\", \"activate\", \"foo\"], obj=project)\n    assert result.exit_code != 0\n    assert \"No virtualenv with key\" in result.stderr\n\n    project._saved_python = os.path.abspath(\"fake/bin/python\")\n    result = pdm([\"venv\", \"activate\"], obj=project)\n    assert result.exit_code != 0, result.output + result.stderr\n    assert \"Can't activate a non-venv Python\" in result.stderr\n\n\n@pytest.mark.usefixtures(\"venv_backends\")\ndef test_venv_activate_no_shell(pdm, mocker, project):\n    project.project_config[\"venv.in_project\"] = False\n    result = pdm([\"venv\", \"create\"], obj=project)\n    assert result.exit_code == 0, result.stderr\n    venv_path = re.match(r\"Virtualenv (.+) is created successfully\", result.output).group(1)\n    key = os.path.basename(venv_path)[len(get_venv_prefix(project)) :]\n\n    mocker.patch(\"shellingham.detect_shell\", side_effect=shellingham.ShellDetectionFailure())\n    result = pdm([\"venv\", \"activate\", key], obj=project)\n    assert result.exit_code == 0, result.stderr\n    backend = project.config[\"venv.backend\"]\n\n    if backend == \"conda\":\n        assert result.output.startswith(\"conda activate\")\n    else:\n        assert result.output.strip(\"'\\\"\\n\").endswith(\"activate\")\n        if platform.system() == \"Windows\":\n            assert not result.output.startswith(\"source\")\n            assert not result.output.startswith(\"'\")\n        else:\n            assert result.output.startswith(\"source\")\n\n\n@pytest.mark.usefixtures(\"fake_create\")\n@pytest.mark.parametrize(\"keep_pypackages\", [True, False])\ndef test_venv_auto_create(pdm, mocker, project, keep_pypackages):\n    creator = mocker.patch(\"pdm.cli.commands.venv.backends.Backend.create\")\n    project._saved_python = None\n    if keep_pypackages:\n        project.root.joinpath(\"__pypackages__\").mkdir(exist_ok=True)\n    else:\n        shutil.rmtree(project.root / \"__pypackages__\", ignore_errors=True)\n    project.project_config[\"python.use_venv\"] = True\n    pdm([\"install\", \"--no-self\"], obj=project)\n    if keep_pypackages:\n        creator.assert_not_called()\n    else:\n        creator.assert_called_once()\n\n\n@pytest.mark.usefixtures(\"fake_create\")\ndef test_venv_purge(pdm, project):\n    project.project_config[\"venv.in_project\"] = False\n    result = pdm([\"venv\", \"purge\"], obj=project)\n    assert result.exit_code == 0, result.stderr\n\n    result = pdm([\"venv\", \"create\"], obj=project)\n    assert result.exit_code == 0, result.stderr\n    venv_path = re.match(r\"Virtualenv (.+) is created successfully\", result.output).group(1)\n    result = pdm([\"venv\", \"purge\"], input=\"y\", obj=project)\n    assert result.exit_code == 0, result.stderr\n    assert not os.path.exists(venv_path)\n\n\n@pytest.mark.usefixtures(\"fake_create\")\ndef test_venv_purge_force(pdm, project):\n    project.project_config[\"venv.in_project\"] = False\n    result = pdm([\"venv\", \"create\"], obj=project)\n    assert result.exit_code == 0, result.stderr\n    venv_path = re.match(r\"Virtualenv (.+) is created successfully\", result.output).group(1)\n    result = pdm([\"venv\", \"purge\", \"-f\"], obj=project)\n    assert result.exit_code == 0, result.stderr\n    assert not os.path.exists(venv_path)\n\n\nuser_options = [(\"none\", True), (\"0\", False), (\"all\", False)]\n\n\n@pytest.mark.usefixtures(\"venv_backends\")\n@pytest.mark.parametrize(\"user_choices, is_path_exists\", user_options)\ndef test_venv_purge_interactive(pdm, user_choices, is_path_exists, project):\n    project.project_config[\"venv.in_project\"] = False\n    result = pdm([\"venv\", \"create\"], obj=project)\n    assert result.exit_code == 0, result.stderr\n    venv_path = re.match(r\"Virtualenv (.+) is created successfully\", result.output).group(1)\n    result = pdm([\"venv\", \"purge\", \"-i\"], input=user_choices, obj=project)\n    assert result.exit_code == 0, result.stderr\n    assert os.path.exists(venv_path) == is_path_exists\n\n\ndef test_virtualenv_backend_create(project, mocker, with_pip):\n    backend = backends.VirtualenvBackend(project, None)\n    assert backend.ident\n    mock_call = mocker.patch(\"subprocess.check_call\")\n    location = backend.create(with_pip=with_pip)\n    pip_args = [] if with_pip else [\"--no-pip\", \"--no-setuptools\", \"--no-wheel\"]\n    mock_call.assert_called_once_with(\n        [\n            sys.executable,\n            \"-m\",\n            \"virtualenv\",\n            str(location),\n            \"-p\",\n            str(backend._resolved_interpreter.executable),\n            *pip_args,\n        ],\n        stdout=ANY,\n    )\n\n\ndef test_venv_backend_create(project, mocker, with_pip):\n    backend = backends.VenvBackend(project, None)\n    assert backend.ident\n    mock_call = mocker.patch(\"subprocess.check_call\")\n    location = backend.create(with_pip=with_pip)\n    pip_args = [] if with_pip else [\"--without-pip\"]\n    mock_call.assert_called_once_with(\n        [\n            str(backend._resolved_interpreter.executable),\n            \"-m\",\n            \"venv\",\n            str(location),\n            *pip_args,\n        ],\n        stdout=ANY,\n    )\n\n\ndef test_conda_backend_create(project, mocker, with_pip):\n    assert project.python\n    backend = backends.CondaBackend(project, \"3.9\")\n    assert backend.ident == \"3.9\"\n    mock_call = mocker.patch(\"subprocess.check_call\")\n    location = backend.create(with_pip=with_pip)\n    pip_args = [\"pip\"] if with_pip else []\n    mock_call.assert_called_once_with(\n        [\n            \"conda\",\n            \"create\",\n            \"--yes\",\n            \"--prefix\",\n            str(location),\n            \"python=3.9\",\n            *pip_args,\n        ],\n        stdout=ANY,\n    )\n\n    backend = backends.CondaBackend(project, None)\n    python_version = f\"{sys.version_info.major}.{sys.version_info.minor}\"\n    assert backend.ident.startswith(python_version)\n    location = backend.create()\n    mock_call.assert_called_with(\n        [\n            \"conda\",\n            \"create\",\n            \"--yes\",\n            \"--prefix\",\n            str(location),\n            f\"python={python_version}\",\n        ],\n        stdout=ANY,\n    )\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "from __future__ import annotations\n\nimport os\nimport shutil\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Iterable\nfrom urllib.parse import unquote, urlparse\n\nimport pytest\nfrom unearth.vcs import Git, vcs_support\n\nfrom pdm.models.auth import keyring\nfrom pdm.project import Project\nfrom tests import FIXTURES\n\nif TYPE_CHECKING:\n    from pdm.pytest import IndexesDefinition\n\n\nos.environ.update(CI=\"1\", PDM_CHECK_UPDATE=\"0\")\n\npytest_plugins = [\n    \"pdm.pytest\",\n]\n\n\n@pytest.fixture\ndef index() -> dict[str, bytes]:\n    return {}\n\n\n@pytest.fixture(scope=\"session\", autouse=True)\ndef disable_keyring():\n    keyring.enabled = False\n\n\n@pytest.fixture\ndef pypi_indexes(index) -> IndexesDefinition:\n    return {\n        \"http://fixtures.test/\": {\n            \"/\": FIXTURES,\n        },\n        \"https://my.pypi.org/\": (\n            {\n                \"/simple\": FIXTURES / \"index\",\n                \"/json\": FIXTURES / \"json\",\n            },\n            index,\n            True,\n        ),\n    }\n\n\ndef pytest_runtest_setup(item):\n    if \"uv\" in item.keywords and not shutil.which(\"uv\"):\n        pytest.skip(\"uv command not found\")\n\n\nclass MockGit(Git):\n    def fetch_new(self, location, url, rev, args):\n        path = os.path.splitext(os.path.basename(unquote(urlparse(str(url)).path)))[0]\n        mocked_path = FIXTURES / \"projects\" / path\n        shutil.copytree(mocked_path, location)\n\n    def get_revision(self, location: Path) -> str:\n        return \"1234567890abcdef\"\n\n    def is_immutable_revision(self, location, link) -> bool:\n        rev = self.get_url_and_rev_options(link)[1]\n        return rev == \"1234567890abcdef\"\n\n\n@pytest.fixture\ndef repository_pypi_json() -> Path:\n    return FIXTURES / \"pypi.json\"\n\n\n@pytest.fixture(scope=\"session\")\ndef build_env_wheels() -> Iterable[Path]:\n    return [\n        FIXTURES / \"artifacts\" / wheel_name\n        for wheel_name in (\n            \"pdm_pep517-1.0.0-py3-none-any.whl\",\n            \"poetry_core-1.3.2-py3-none-any.whl\",\n            \"setuptools-68.0.0-py3-none-any.whl\",\n            \"wheel-0.37.1-py2.py3-none-any.whl\",\n            \"flit_core-3.6.0-py3-none-any.whl\",\n            \"pdm_backend-2.1.4-py3-none-any.whl\",\n            \"importlib_metadata-4.8.3-py3-none-any.whl\",\n            \"zipp-3.7.0-py3-none-any.whl\",\n            \"typing_extensions-4.4.0-py3-none-any.whl\",\n        )\n    ]\n\n\n@pytest.fixture\ndef local_finder_artifacts() -> Path:\n    return FIXTURES / \"artifacts\"\n\n\ndef copytree(src: Path, dst: Path) -> None:\n    if not dst.exists():\n        dst.mkdir(parents=True)\n    for subpath in src.iterdir():\n        if subpath.is_dir():\n            copytree(subpath, dst / subpath.name)\n        else:\n            shutil.copy2(subpath, dst)\n\n\n@pytest.fixture()\ndef fixture_project(project_no_init: Project, request: pytest.FixtureRequest, local_finder_artifacts: Path):\n    \"\"\"Initialize a project from a fixture project\"\"\"\n\n    def func(project_name):\n        source = FIXTURES / \"projects\" / project_name\n        copytree(source, project_no_init.root)\n        project_no_init.pyproject.reload()\n        project_no_init.pyproject.open_for_write()\n        if \"local_finder\" in request.fixturenames:\n            project_no_init.pyproject.settings[\"source\"] = [\n                {\n                    \"type\": \"find_links\",\n                    \"verify_ssl\": False,\n                    \"url\": local_finder_artifacts.as_uri(),\n                    \"name\": \"pypi\",\n                }\n            ]\n            project_no_init.pyproject.write()\n        project_no_init.environment = None\n        return project_no_init\n\n    return func\n\n\n@pytest.fixture()\ndef vcs(monkeypatch):\n    monkeypatch.setattr(vcs_support, \"_registry\", {\"git\": MockGit})\n    return\n\n\n@pytest.fixture(params=[False, True])\ndef is_editable(request):\n    return request.param\n\n\n@pytest.fixture(params=[False, True])\ndef dev_option(request) -> Iterable[str]:\n    return (\"--dev\",) if request.param else ()\n"
  },
  {
    "path": "tests/environments/test_environment.py",
    "content": "import os\nimport sys\nfrom pathlib import Path\nfrom types import ModuleType, SimpleNamespace\n\nimport pytest\n\nfrom pdm.environments.local import PythonLocalEnvironment\nfrom pdm.utils import pdm_scheme\n\n\n@pytest.fixture()\ndef local_env(project):\n    # Ensure we construct a fresh local environment for each test\n    env = PythonLocalEnvironment(project)\n    return env\n\n\ndef test_packages_path_compat_suffix_32(local_env, tmp_path, monkeypatch):\n    # Simulate interpreter identifier ending with -32, and only non-\"-32\" directory exists\n    monkeypatch.setattr(local_env, \"_interpreter\", SimpleNamespace(identifier=\"3.11-32\"))\n    base = local_env.project.root / \"__pypackages__\"\n    compat_dir = base / \"3.11\"\n    compat_dir.mkdir(parents=True, exist_ok=True)\n    # Also ensure parent exists for side effects inside packages_path\n    p = local_env.packages_path\n    assert p.name == \"3.11\"\n    assert p.parent == base\n\n\ndef test_local_get_paths_headers_override(local_env):\n    paths = local_env.get_paths(dist_name=\"mypkg\")\n    # Ensure headers path is under include/mypkg (cross-platform path check)\n    from pathlib import Path as _P\n\n    assert _P(paths[\"headers\"]).parts[-2:] == (\"include\", \"mypkg\")\n    # Sanity: scheme base is pep582\n    scheme = pdm_scheme(local_env.packages_path.as_posix())\n    for k in (\"purelib\", \"platlib\", \"scripts\", \"data\"):\n        assert paths[k].startswith(scheme[k])\n\n\ndef test_pip_command_uses_existing_module(monkeypatch, project):\n    # Simulate: `python -Esm pip --version` fails, but host pip is compatible\n    env = PythonLocalEnvironment(project)\n\n    class DummyCompleted:\n        returncode = 1\n\n    monkeypatch.setattr(\"subprocess.run\", lambda *a, **k: DummyCompleted())\n\n    # Provide a dummy pip module with a file path\n    dummy_pip = ModuleType(\"pip\")\n    dummy_dir = Path(project.core.create_temp_dir(prefix=\"pip-test-\"))\n    dummy_file = dummy_dir / \"__init__.py\"\n    dummy_file.write_text(\"\")\n    dummy_pip.__file__ = str(dummy_file)\n    monkeypatch.setitem(sys.modules, \"pip\", dummy_pip)\n\n    # Make it considered compatible; patch the symbol used in BaseEnvironment\n    monkeypatch.setattr(\"pdm.environments.base.is_pip_compatible_with_python\", lambda v: True)\n\n    cmd = env.pip_command\n    assert cmd[:3] == [str(env.interpreter.executable), \"-Es\", str(dummy_dir)]\n\n\ndef test_pip_command_download_fallback(monkeypatch, project):\n    # Simulate: `python -Esm pip --version` fails and host pip is unavailable/incompatible\n    env = PythonLocalEnvironment(project)\n\n    class DummyCompleted:\n        returncode = 1\n\n    monkeypatch.setattr(\"subprocess.run\", lambda *a, **k: DummyCompleted())\n\n    # Force importing pip to fail inside BaseEnvironment so pip_location is None\n    import builtins as _builtins\n\n    _real_import = _builtins.__import__\n\n    def _fake_import(name, globals=None, locals=None, fromlist=(), level=0):\n        if name == \"pip\" or name.startswith(\"pip.\"):\n            raise ImportError\n        return _real_import(name, globals, locals, fromlist, level)\n\n    monkeypatch.setattr(\"builtins.__import__\", _fake_import)\n\n    # Also ensure compatibility check returns False on the symbol used in BaseEnvironment\n    monkeypatch.setattr(\"pdm.environments.base.is_pip_compatible_with_python\", lambda v: False)\n\n    def fake_download(path):\n        # Create the expected wheel path\n        Path(path).write_text(\"\")\n\n    # Avoid network: stub out the download function to just create the file\n    monkeypatch.setattr(type(env), \"_download_pip_wheel\", lambda self, p: fake_download(p))\n\n    cmd = env.pip_command\n    # Expect the -m form not used, fallback to the wheel's pip script\n    assert cmd[0] == str(env.interpreter.executable)\n    assert Path(cmd[1]).name == \"pip\"\n\n\ndef test_pip_command_installed(monkeypatch, project):\n    # Simulate: `python -Esm pip --version` succeeds -> use it directly\n    env = PythonLocalEnvironment(project)\n\n    class DummyCompleted:\n        returncode = 0\n\n    monkeypatch.setattr(\"subprocess.run\", lambda *a, **k: DummyCompleted())\n\n    cmd = env.pip_command\n    assert cmd[:3] == [str(env.interpreter.executable), \"-Esm\", \"pip\"]\n\n\ndef test_script_kind_posix(local_env):\n    # On non-Windows platforms, script_kind should be posix\n    if os.name != \"nt\":\n        assert local_env.script_kind == \"posix\"\n\n\ndef test_which_python_variants(local_env):\n    # Should resolve to interpreter path when asking for pythonN or python\n    exe = str(local_env.interpreter.executable)\n    assert local_env.which(\"python\") == exe\n    # python3 matches the major version\n    assert local_env.which(f\"python{local_env.interpreter.version.major}\") == exe\n\n\ndef test_process_env_includes_scripts_first(local_env):\n    env = local_env.process_env\n    scripts = local_env.get_paths()[\"scripts\"]\n    path_entries = env[\"PATH\"].split(os.pathsep)\n    assert path_entries[0] == scripts\n"
  },
  {
    "path": "tests/environments/test_shebangs.py",
    "content": "import io\nimport os\nimport shlex\nimport zipfile\nfrom pathlib import Path\n\nimport pytest\n\nfrom pdm.environments.local import PythonLocalEnvironment, _get_shebang_path, _is_console_script, _replace_shebang\n\n\n@pytest.mark.parametrize(\n    \"executable,is_launcher,expected\",\n    [\n        (\"/usr/bin/python\", True, b\"/usr/bin/python\"),\n        (\"/usr/bin/python\", False, b\"/usr/bin/python\"),\n        (\"/a path/with space/python\", False, shlex.quote(\"/a path/with space/python\").encode(\"utf-8\")),\n    ],\n)\ndef test_get_shebang_path(executable, is_launcher, expected):\n    assert _get_shebang_path(executable, is_launcher) == expected\n\n\ndef test_is_console_script_unix_and_binary():\n    # Non-Windows path: startswith #! => True\n    assert _is_console_script(b\"#!/usr/bin/env python\\nprint('x')\\n\") is True\n    # Undecodable bytes => False\n    assert _is_console_script(b\"\\xff\\xfe\\xfd\") is False\n\n\n@pytest.mark.skipif(os.name == \"nt\", reason=\"Regex branch differs on Windows\")\n@pytest.mark.parametrize(\n    \"content,new_exec,expected_prefix\",\n    [\n        (b\"#!/usr/bin/python\\nprint('hi')\\n\", b\"/new/python\", b\"#!/new/python\\n\"),\n        (\n            b\"#!/bin/sh\\n'''exec' '/old path/python' \\\"$0\\\" \\\"$@\\\"\\n' '''\\nprint('x')\\n\",\n            b\"/new/python\",\n            b\"#!/bin/sh\\n'''exec' /new/python \\\"$0\\\"\",\n        ),\n    ],\n)\ndef test_replace_shebang_unix(tmp_path: Path, content: bytes, new_exec: bytes, expected_prefix: bytes):\n    f = tmp_path / \"script\"\n    f.write_bytes(content)\n    _replace_shebang(f, new_exec)\n    data = f.read_bytes()\n    assert data.startswith(expected_prefix)\n\n\ndef test_update_shebangs_changes_scripts_header(project):\n    env = PythonLocalEnvironment(project)\n    # Create a fake script under the environment's scripts dir\n    scripts = Path(env.get_paths()[\"scripts\"])  # ensure exists\n    script = scripts / \"demo\"\n    script.write_text(\"#!/usr/bin/python\\nprint('ok')\\n\", encoding=\"utf-8\")\n\n    new_path = \"/opt/python/bin/python\"\n    # Exercise update_shebangs\n    env.update_shebangs(new_path)\n\n    text = script.read_text(encoding=\"utf-8\")\n    assert text.splitlines()[0] == f\"#!{new_path}\"\n\n\ndef test_update_shebangs_ignores_non_target_files_and_dirs(project):\n    env = PythonLocalEnvironment(project)\n    scripts = Path(env.get_paths()[\"scripts\"])  # ensure exists\n    # A non-target extension file\n    other = scripts / \"notascript.sh\"\n    other.write_text(\"#!/usr/bin/python\\nprint('no change')\\n\", encoding=\"utf-8\")\n    # A directory\n    d = scripts / \"adir\"\n    d.mkdir(exist_ok=True)\n\n    before_other = other.read_text(encoding=\"utf-8\")\n    env.update_shebangs(\"/opt/python/bin/python\")\n    after_other = other.read_text(encoding=\"utf-8\")\n    # Should be unchanged\n    assert before_other == after_other\n\n\ndef test_replace_shebang_early_return_when_not_console(tmp_path: Path):\n    f = tmp_path / \"no_shebang\"\n    original = b\"print('hello')\\n\"\n    f.write_bytes(original)\n    _replace_shebang(f, b\"/new/python\")\n    assert f.read_bytes() == original\n\n\ndef _zip_with_main_bytes() -> bytes:\n    buf = io.BytesIO()\n    with zipfile.ZipFile(buf, mode=\"w\") as zf:\n        zf.writestr(\"__main__.py\", \"print('ok')\\n\")\n    return buf.getvalue()\n\n\ndef test_is_console_script_windows_zip(monkeypatch):\n    monkeypatch.setattr(os, \"name\", \"nt\")\n    data = _zip_with_main_bytes()\n    assert _is_console_script(data) is True\n    # Not a valid zip => False (falls back to text detection which returns False)\n    assert _is_console_script(b\"not a zip\") is False\n\n\ndef test_is_console_script_windows_text_shebang(monkeypatch):\n    # On Windows, non-zip scripts with shebang should still be recognized\n    monkeypatch.setattr(os, \"name\", \"nt\")\n    assert _is_console_script(b\"#!/usr/bin/env python\\r\\nprint('x')\\r\\n\") is True\n\n\ndef test_replace_shebang_windows_zip_no_change(monkeypatch, tmp_path: Path):\n    monkeypatch.setattr(os, \"name\", \"nt\")\n    f = tmp_path / \"script.exe\"\n    data = _zip_with_main_bytes()\n    f.write_bytes(data)\n    _replace_shebang(f, b\"C:/Python/python.exe\")\n    # Should remain unchanged since regex won't match zip content\n    assert f.read_bytes() == data\n"
  },
  {
    "path": "tests/fixtures/Pipfile",
    "content": "[[source]]\nurl = \"https://pypi.python.org/simple\"\nverify_ssl = true\nname = \"pypi\"\n\n[packages]\nrequests = \"*\"\npywinusb = {version = \"*\", sys_platform = \"== 'win32'\"}\n\n[pipenv]\nallow_prereleases = true\n\n[requires]\npython_version = \"3.6\"\n"
  },
  {
    "path": "tests/fixtures/__init__.py",
    "content": ""
  },
  {
    "path": "tests/fixtures/constraints.txt",
    "content": "# This is a pip constraints file\nrequests==2.20.0b1\ndjango==1.11.8\ncertifi==2018.11.17\nchardet==3.0.4\nidna==2.7\npytz==2019.3\nurllib3==1.23b0\n"
  },
  {
    "path": "tests/fixtures/index/demo.html",
    "content": "<!DOCTYPE html>\n<html>\n  <body>\n    <h1>Demo</h1>\n    <a href=\"http://fixtures.test/artifacts/demo-0.0.1-cp36-cp36m-win_amd64.whl\">\n      demo-0.0.1-cp36-cp36m-win_amd64.whl\n    </a>\n    <a href=\"http://fixtures.test/artifacts/demo-0.0.1-py2.py3-none-any.whl\">\n      demo-0.0.1-py2.py3-none-any.whl\n    </a>\n  </body>\n</html>\n"
  },
  {
    "path": "tests/fixtures/index/future-fstrings.html",
    "content": "<!DOCTYPE html>\n<html>\n  <body>\n    <h1>future-fstrings</h1>\n    <a\n      href=\"http://fixtures.test/artifacts/future_fstrings-1.2.0-py2.py3-none-any.whl#sha256=90e49598b553d8746c4dc7d9442e0359d038c3039d802c91c0a55505da318c63\"\n    >\n      future_fstrings-1.2.0-py2.py3-none-any.whl\n    </a>\n    <a\n      href=\"http://fixtures.test/artifacts/future_fstrings-1.2.0.tar.gz#sha256=6cf41cbe97c398ab5a81168ce0dbb8ad95862d3caf23c21e4430627b90844089\"\n    >\n      future_fstrings-1.2.0.tar.gz\n    </a>\n  </body>\n</html>\n"
  },
  {
    "path": "tests/fixtures/index/pep345-legacy.html",
    "content": "<!DOCTYPE html>\n<html>\n  <body>\n    <h1>pep345-legacy</h1>\n    <a\n      href=\"http://fixtures.test/artifacts/pep345_legacy-0.0.1-py2.py3-none-any.whl\"\n      data-requires-python=\"3\"\n    >\n      pep345_legacy-0.0.1-py2.py3-none-any.whl\n    </a>\n  </body>\n</html>\n"
  },
  {
    "path": "tests/fixtures/index/wheel.html",
    "content": "<!DOCTYPE html>\n<html>\n  <body>\n    <h1>wheel</h1>\n    <a\n      href=\"http://fixtures.test/artifacts/wheel-0.37.1-py2.py3-none-any.whl\"\n      data-requires-python=\">=3.1.*'\"\n    >\n      wheel-0.37.1-py2.py3-none-any.whl\n    </a>\n  </body>\n</html>\n"
  },
  {
    "path": "tests/fixtures/json/zipp.json",
    "content": "{\n  \"meta\": {\n    \"api-version\": \"1.0\"\n  },\n  \"name\": \"zipp\",\n  \"files\": [\n    {\n      \"filename\": \"zipp-3.6.0-py3-none-any.whl\",\n      \"url\": \"http://fixtures.test/artifacts/zipp-3.6.0-py3-none-any.whl\",\n      \"hashes\": {\n        \"sha256\": \"9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc\"\n      },\n      \"upload-time\": \"2023-02-01T00:00:00.000000Z\",\n      \"requires-python\": \">=3.7\"\n    },\n    {\n      \"filename\": \"zipp-3.7.0-py3-none-any.whl\",\n      \"url\": \"http://fixtures.test/artifacts/zipp-3.7.0-py3-none-any.whl\",\n      \"hashes\": {\n        \"sha256\": \"b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375\"\n      },\n      \"upload-time\": \"2024-02-01T00:00:00.000000Z\",\n      \"requires-python\": \">=3.7\"\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/fixtures/poetry-error.toml",
    "content": "[tool.poetry]\nname = \"test-poetry\"\nversion = \"0.1.0\"\ndescription = \"\"\nauthors = [\"Frost Ming <me@frostming.com>\"]\nreadme = \"README.md\"\n\n[tool.poetry.dependencies]\npython = \"^3.11\"\nfoo = \">=1.0||^2.1\"\n\n[build-system]\nrequires = [\"poetry-core\"]\nbuild-backend = \"poetry.core.masonry.api\"\n"
  },
  {
    "path": "tests/fixtures/poetry-new.toml",
    "content": "[tool.poetry]\nname = \"test-poetry\"\nversion = \"0.1.0\"\ndescription = \"\"\nauthors = [\"Frost Ming <me@frostming.com>\"]\nreadme = \"README.md\"\npackages = [{include = \"test_poetry\"}]\n\n[tool.poetry.dependencies]\npython = \"^3.9\"\nhttpx = \"*\"\npendulum = \"*\"\n\n[tool.poetry.group.test.dependencies]\npytest = \"^6.0.0\"\npytest-mock = \"*\"\n\n[build-system]\nrequires = [\"poetry-core\"]\nbuild-backend = \"poetry.core.masonry.api\"\n"
  },
  {
    "path": "tests/fixtures/projects/__init__.py",
    "content": ""
  },
  {
    "path": "tests/fixtures/projects/demo/demo.py",
    "content": "import os\n\nimport chardet\n\nprint(os.name)\n"
  },
  {
    "path": "tests/fixtures/projects/demo/pylock.toml",
    "content": "lock-version = \"1.0\"\nrequires-python = \">=3.3\"\nenvironments = [\n    \"python_version >= \\\"3.3\\\"\",\n]\nextras = [\"security\", \"tests\"]\ndependency-groups = [\"default\"]\ndefault-groups = [\"default\"]\ncreated-by = \"pdm\"\n[[packages]]\nname = \"certifi\"\nversion = \"2025.1.31\"\nrequires-python = \">=3.6\"\nsdist = {url = \"https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz\", hashes = {sha256 = \"3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651\"}}\nwheels = [\n    {url = \"https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl\",hashes = {sha256 = \"ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe\"}},\n]\nmarker = \"python_version >= \\\"3.6\\\" and \\\"security\\\" in extras\"\n\n[packages.tool.pdm]\ndependencies = []\n\n[[packages]]\nname = \"chardet\"\nversion = \"3.0.4\"\nsdist = {url = \"https://files.pythonhosted.org/packages/fc/bb/a5768c230f9ddb03acc9ef3f0d4a3cf93462473795d18e9535498c8f929d/chardet-3.0.4.tar.gz\", hashes = {sha256 = \"84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae\"}}\nwheels = [\n    {url = \"https://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec7510b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl\",hashes = {sha256 = \"fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691\"}},\n]\nmarker = \"os_name == \\\"nt\\\" and \\\"default\\\" in dependency_groups\"\n\n[packages.tool.pdm]\ndependencies = []\n\n[[packages]]\nname = \"charset-normalizer\"\nversion = \"2.0.12\"\nrequires-python = \">=3.5.0\"\nsdist = {url = \"https://files.pythonhosted.org/packages/56/31/7bcaf657fafb3c6db8c787a865434290b726653c912085fbd371e9b92e1c/charset-normalizer-2.0.12.tar.gz\", hashes = {sha256 = \"2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597\"}}\nwheels = [\n    {url = \"https://files.pythonhosted.org/packages/06/b3/24afc8868eba069a7f03650ac750a778862dc34941a4bebeb58706715726/charset_normalizer-2.0.12-py3-none-any.whl\",hashes = {sha256 = \"6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df\"}},\n]\nmarker = \"python_version >= \\\"3.6\\\" and \\\"security\\\" in extras\"\n\n[packages.tool.pdm]\ndependencies = []\n\n[[packages]]\nname = \"colorama\"\nversion = \"0.3.9\"\nsdist = {url = \"https://files.pythonhosted.org/packages/e6/76/257b53926889e2835355d74fec73d82662100135293e17d382e2b74d1669/colorama-0.3.9.tar.gz\", hashes = {sha256 = \"48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1\"}}\nwheels = [\n    {url = \"https://files.pythonhosted.org/packages/db/c8/7dcf9dbcb22429512708fe3a547f8b6101c0d02137acbd892505aee57adf/colorama-0.3.9-py2.py3-none-any.whl\",hashes = {sha256 = \"463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda\"}},\n]\nmarker = \"sys_platform == \\\"win32\\\" and \\\"tests\\\" in extras\"\n\n[packages.tool.pdm]\ndependencies = []\n\n[[packages]]\nname = \"idna\"\nversion = \"2.7\"\nsdist = {url = \"https://files.pythonhosted.org/packages/65/c4/80f97e9c9628f3cac9b98bfca0402ede54e0563b56482e3e6e45c43c4935/idna-2.7.tar.gz\", hashes = {sha256 = \"684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16\"}}\nwheels = [\n    {url = \"https://files.pythonhosted.org/packages/4b/2a/0276479a4b3caeb8a8c1af2f8e4355746a97fab05a372e4a2c6a6b876165/idna-2.7-py2.py3-none-any.whl\",hashes = {sha256 = \"156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e\"}},\n]\nmarker = \"\\\"default\\\" in dependency_groups or \\\"security\\\" in extras\"\n\n[packages.tool.pdm]\ndependencies = []\n\n[[packages]]\nname = \"py\"\nversion = \"1.4.34\"\nsdist = {url = \"https://files.pythonhosted.org/packages/68/35/58572278f1c097b403879c1e9369069633d1cbad5239b9057944bb764782/py-1.4.34.tar.gz\", hashes = {sha256 = \"0f2d585d22050e90c7d293b6451c83db097df77871974d90efd5a30dc12fcde3\"}}\nwheels = [\n    {url = \"https://files.pythonhosted.org/packages/53/67/9620edf7803ab867b175e4fd23c7b8bd8eba11cb761514dcd2e726ef07da/py-1.4.34-py2.py3-none-any.whl\",hashes = {sha256 = \"2ccb79b01769d99115aa600d7eed99f524bf752bba8f041dc1c184853514655a\"}},\n]\nmarker = \"\\\"tests\\\" in extras\"\n\n[packages.tool.pdm]\ndependencies = []\n\n[[packages]]\nname = \"pytest\"\nversion = \"3.2.5\"\nsdist = {url = \"https://files.pythonhosted.org/packages/1f/f8/8cd74c16952163ce0db0bd95fdd8810cbf093c08be00e6e665ebf0dc3138/pytest-3.2.5.tar.gz\", hashes = {sha256 = \"6d5bd4f7113b444c55a3bbb5c738a3dd80d43563d063fc42dcb0aaefbdd78b81\"}}\nwheels = [\n    {url = \"https://files.pythonhosted.org/packages/ef/41/d8a61f1b2ba308e96b36106e95024977e30129355fd12087f23e4b9852a1/pytest-3.2.5-py2.py3-none-any.whl\",hashes = {sha256 = \"241d7e7798d79192a123ceaf64c602b4d233eacf6d6e42ae27caa97f498b7dc6\"}},\n]\nmarker = \"\\\"tests\\\" in extras\"\n\n[packages.tool.pdm]\ndependencies = [\n    \"argparse; python_version == \\\"2.6\\\"\",\n    \"colorama; sys_platform == \\\"win32\\\"\",\n    \"ordereddict; python_version == \\\"2.6\\\"\",\n    \"py>=1.4.33\",\n    \"setuptools\",\n]\n\n[[packages]]\nname = \"requests\"\nversion = \"2.27.1\"\nrequires-python = \">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*\"\nsdist = {url = \"https://files.pythonhosted.org/packages/60/f3/26ff3767f099b73e0efa138a9998da67890793bfa475d8278f84a30fec77/requests-2.27.1.tar.gz\", hashes = {sha256 = \"68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61\"}}\nwheels = [\n    {url = \"https://files.pythonhosted.org/packages/2d/61/08076519c80041bc0ffa1a8af0cbd3bf3e2b62af10435d269a9d0f40564d/requests-2.27.1-py2.py3-none-any.whl\",hashes = {sha256 = \"f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d\"}},\n]\nmarker = \"python_version >= \\\"3.6\\\" and \\\"security\\\" in extras\"\n\n[packages.tool.pdm]\ndependencies = [\n    \"certifi>=2017.4.17\",\n    \"chardet<5,>=3.0.2; python_version < \\\"3\\\"\",\n    \"charset-normalizer~=2.0.0; python_version >= \\\"3\\\"\",\n    \"idna<3,>=2.5; python_version < \\\"3\\\"\",\n    \"idna<4,>=2.5; python_version >= \\\"3\\\"\",\n    \"urllib3<1.27,>=1.21.1\",\n]\n\n[[packages]]\nname = \"setuptools\"\nversion = \"39.2.0\"\nrequires-python = \">=2.7,!=3.0.*,!=3.1.*,!=3.2.*\"\nsdist = {url = \"https://files.pythonhosted.org/packages/1a/04/d6f1159feaccdfc508517dba1929eb93a2854de729fa68da9d5c6b48fa00/setuptools-39.2.0.zip\", hashes = {sha256 = \"f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2\"}}\nwheels = [\n    {url = \"https://files.pythonhosted.org/packages/7f/e1/820d941153923aac1d49d7fc37e17b6e73bfbd2904959fffbad77900cf92/setuptools-39.2.0-py2.py3-none-any.whl\",hashes = {sha256 = \"8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926\"}},\n]\nmarker = \"\\\"tests\\\" in extras\"\n\n[packages.tool.pdm]\ndependencies = []\n\n[[packages]]\nname = \"urllib3\"\nversion = \"1.26.20\"\nrequires-python = \"!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7\"\nsdist = {url = \"https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz\", hashes = {sha256 = \"40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32\"}}\nwheels = [\n    {url = \"https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl\",hashes = {sha256 = \"0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e\"}},\n]\nmarker = \"python_version >= \\\"3.6\\\" and \\\"security\\\" in extras\"\n\n[packages.tool.pdm]\ndependencies = []\n\n[tool.pdm]\nhashes = {sha256 = \"2584886ac58a0ae70aa36bc0318b62c3e2c89acc9c21ebb9aee74147c0a9dc06\"}\n\n[[tool.pdm.targets]]\nrequires_python = \">=3.3\"\n"
  },
  {
    "path": "tests/fixtures/projects/demo/pyproject.toml",
    "content": "\n[project]\nname = \"demo\"\nversion = \"0.0.1\"\ndescription = \"test demo\"\nrequires-python = \">=3.3\"\ndependencies = [\n    \"idna\",\n    \"chardet; os_name=='nt'\",\n]\n\n[project.optional-dependencies]\ntests = [\n    \"pytest\",\n]\nsecurity = [\n    \"requests; python_version>=\\\"3.6\\\"\",\n]\n\n[build-system]\nrequires = [\"pdm-backend\"]\nbuild-backend = \"pdm.backend\"\n"
  },
  {
    "path": "tests/fixtures/projects/demo-#-with-hash/demo.py",
    "content": "import os\n\nimport chardet\n\nprint(os.name)\n"
  },
  {
    "path": "tests/fixtures/projects/demo-#-with-hash/setup.py",
    "content": "from setuptools import setup\n\n\nsetup(\n    name=\"demo\",\n    version=\"0.0.1\",\n    description=\"test demo\",\n    py_modules=[\"demo\"],\n    python_requires=\">=3.3\",\n    install_requires=[\"idna\", \"chardet; os_name=='nt'\"],\n    extras_require={\n        \"tests\": [\"pytest\"],\n        \"security\": ['requests; python_version>=\"3.6\"'],\n    },\n)\n"
  },
  {
    "path": "tests/fixtures/projects/demo-combined-extras/demo.py",
    "content": "import os\n\nprint(os.name)\n"
  },
  {
    "path": "tests/fixtures/projects/demo-combined-extras/pyproject.toml",
    "content": "[project]\n# PEP 621 project metadata\n# See https://www.python.org/dev/peps/pep-0621/\nauthors = [\n    {name = \"frostming\", email = \"mianghong@gmail.com\"},\n]\nversion = \"0.1.0\"\nrequires-python = \">=3.5\"\nlicense = {text = \"MIT\"}\ndependencies = [\"urllib3\"]\ndescription = \"\"\nname = \"demo-package-extra\"\n\n[project.optional-dependencies]\nbe = [\"idna\"]\nte = [\"chardet\"]\nall = [\"idna\", \"chardet\"]\n\n[build-system]\nrequires = [\"pdm-backend\"]\nbuild-backend = \"pdm.backend\"\n"
  },
  {
    "path": "tests/fixtures/projects/demo-failure/demo.py",
    "content": "import os\n\nimport chardet\n\nprint(os.name)\n"
  },
  {
    "path": "tests/fixtures/projects/demo-failure/setup.py",
    "content": "from setuptools import setup\n\nimport first\n\nsetup(\n    name=\"demo\",\n    version=\"0.0.1\",\n    description=\"test demo\",\n    py_modules=[\"demo\"],\n    python_requires=\">=3.3\",\n    install_requires=[\"idna\", \"chardet; os_name=='nt'\"],\n    extras_require={\n        \"tests\": [\"pytest\"],\n        \"security\": ['requests; python_version>=\"3.6\"'],\n    },\n)\n"
  },
  {
    "path": "tests/fixtures/projects/demo-failure-no-dep/demo.py",
    "content": "import os\n\nimport chardet\n\nprint(os.name)\n"
  },
  {
    "path": "tests/fixtures/projects/demo-failure-no-dep/setup.py",
    "content": "from setuptools import setup\n\nif True:\n    raise RuntimeError(\"This mimics the build error on unmatched platform\")\n\nsetup(\n    name=\"demo\",\n    version=\"0.0.1\",\n    description=\"test demo\",\n    py_modules=[\"demo\"],\n    python_requires=\">=3.3\",\n)\n"
  },
  {
    "path": "tests/fixtures/projects/demo-module/LICENSE",
    "content": "MIT License\n"
  },
  {
    "path": "tests/fixtures/projects/demo-module/README.md",
    "content": "# This is a demo module\n"
  },
  {
    "path": "tests/fixtures/projects/demo-module/bar_module.py",
    "content": "bar = \"Hello\"\n"
  },
  {
    "path": "tests/fixtures/projects/demo-module/foo_module.py",
    "content": "__version__ = \"0.1.0\"\nfoo = \"hello\"\n"
  },
  {
    "path": "tests/fixtures/projects/demo-module/pyproject.toml",
    "content": "[build-system]\nrequires = [\"pdm-backend\"]\nbuild-backend = \"pdm.backend\"\n\n[project]\n# PEP 621 project metadata\n# See https://www.python.org/dev/peps/pep-0621/\nauthors = [\n    {name = \"frostming\", email = \"mianghong@gmail.com\"},\n]\ndynamic = [\"version\"]\nrequires-python = \">=3.5\"\nlicense = {text = \"MIT\"}\ndependencies = []\ndescription = \"\"\nname = \"demo-module\"\n\n[project.optional-dependencies]\n\n[tool.pdm.version]\nsource = \"file\"\npath = \"foo_module.py\"\n"
  },
  {
    "path": "tests/fixtures/projects/demo-package/LICENSE",
    "content": "MIT License\n"
  },
  {
    "path": "tests/fixtures/projects/demo-package/README.md",
    "content": "# my-package\n"
  },
  {
    "path": "tests/fixtures/projects/demo-package/data_out.json",
    "content": "{\"name\": \"foo\"}\n"
  },
  {
    "path": "tests/fixtures/projects/demo-package/my_package/__init__.py",
    "content": "__version__ = \"0.1.0\"\n"
  },
  {
    "path": "tests/fixtures/projects/demo-package/my_package/data.json",
    "content": "{\"name\": \"demo-module\"}\n"
  },
  {
    "path": "tests/fixtures/projects/demo-package/pyproject.toml",
    "content": "[build-system]\nrequires = [\"pdm-backend\"]\nbuild-backend = \"pdm.backend\"\n\n[project]\n# PEP 621 project metadata\n# See https://www.python.org/dev/peps/pep-0621/\nauthors = [\n    {name = \"frostming\", email = \"mianghong@gmail.com\"},\n]\ndynamic = [\"version\"]\nrequires-python = \">=3.5\"\nlicense = {text = \"MIT\"}\ndependencies = [\"flask\"]\ndescription = \"\"\nname = \"my-package\"\nreadme = \"README.md\"\n\n[project.optional-dependencies]\n\n[tool.pdm.version]\nsource = \"file\"\npath = \"my_package/__init__.py\"\n\n[[tool.pdm.source]]\nurl = \"https://test.pypi.org/simple\"\nverify_ssl = true\nname = \"testpypi\"\n"
  },
  {
    "path": "tests/fixtures/projects/demo-package/requirements.ini",
    "content": "# This file is @generated by PDM.\n# Please do not edit it manually.\n\nflask\n--extra-index-url https://test.pypi.org/simple\n"
  },
  {
    "path": "tests/fixtures/projects/demo-package/requirements.txt",
    "content": "# This file is @generated by PDM.\n# Please do not edit it manually.\n\nclick==7.1.2 \\\n    --hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a \\\n    --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc\nflask==1.1.4 \\\n    --hash=sha256:0fbeb6180d383a9186d0d6ed954e0042ad9f18e0e8de088b2b419d526927d196 \\\n    --hash=sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22\nitsdangerous==1.1.0 \\\n    --hash=sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19 \\\n    --hash=sha256:72a1252c0b2cc2bcc351acf2cfe2ec0159d8578c54767d5c2aa67fd869346e55 \\\n    --hash=sha256:ac4c9f590d59c36b7d2953f97fda415f2461280e5279650aafe1e593f129c4f7 \\\n    --hash=sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749\njinja2==2.11.3 \\\n    --hash=sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419 \\\n    --hash=sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6\nmarkupsafe==1.1.1 \\\n    --hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \\\n    --hash=sha256:0db2ff381c2218c1ba7192f75e5c5cf180efa023ddfc6914ffe9a38b2bd303dd \\\n    --hash=sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42 \\\n    --hash=sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f \\\n    --hash=sha256:1e8a27f1c41cae51e8a5687aa63d2bf92dc96bebd6d5b33cc3ec8fa31071ee9b \\\n    --hash=sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39 \\\n    --hash=sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff \\\n    --hash=sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b \\\n    --hash=sha256:2a5df1859f9d06d414e60f793466b2549be4ea5fce872d8750215d0b22b4003c \\\n    --hash=sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014 \\\n    --hash=sha256:2d3aa974bd1ee74affb31503dfe3ce55bffd208ef5d5dd1582a1300efbc232f4 \\\n    --hash=sha256:30323461f382a59bcb940be0adc5e0d6be5dfc6bd1c2c5cbe2d13b96414e1619 \\\n    --hash=sha256:37fd53e8825dd3f3c6f6746afa8373b623a87fc0568513f1a5f071ce73ceedb0 \\\n    --hash=sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f \\\n    --hash=sha256:3db7af9025f66e08c781093dbdb7b54e52b5506006e141dcbe5b740e578b5504 \\\n    --hash=sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e \\\n    --hash=sha256:5079d5041ace388bb57a5ebe38ae585fb18bc681a669030d76f99b510b33d53e \\\n    --hash=sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66 \\\n    --hash=sha256:5881c17ae0378a26e2a8abde240387ca44ac778e0a8745b55522c4119d24dfff \\\n    --hash=sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b \\\n    --hash=sha256:5eba30ae5ad72351903ba340a6b4e427353de04542f36fc177ebaffafb8ae5e7 \\\n    --hash=sha256:60f1467a898ed3402d2f4e679906aed9f55c14d6990a53c3f811f593ee425a88 \\\n    --hash=sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15 \\\n    --hash=sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1 \\\n    --hash=sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85 \\\n    --hash=sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1 \\\n    --hash=sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e \\\n    --hash=sha256:753e2b8c825b41dc3aec6c08a6f2eb786a8a3b266258e391375038f511fc4518 \\\n    --hash=sha256:780c6d675155c54556d877df22e4739295a085618ad8bc2d1d25ba07879c9522 \\\n    --hash=sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b \\\n    --hash=sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905 \\\n    --hash=sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850 \\\n    --hash=sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0 \\\n    --hash=sha256:8b7fb692d27d6e17ca0fbcbc0edd7b32790e7c070624211499db5a758e89e38d \\\n    --hash=sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d \\\n    --hash=sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb \\\n    --hash=sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d \\\n    --hash=sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c \\\n    --hash=sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1 \\\n    --hash=sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2 \\\n    --hash=sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2 \\\n    --hash=sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5 \\\n    --hash=sha256:b1423ef6a7f62c1d92bd1ce15e224f8d709fbae969fd7720623eb2ae9b8c3a34 \\\n    --hash=sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7 \\\n    --hash=sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8 \\\n    --hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \\\n    --hash=sha256:bd597f350935cb5c9cc3a55f5a15c8ec3632c71274e079380ef2f4789ad824f0 \\\n    --hash=sha256:bf13467480b37db64550c4f661e4dab34d2ce714d986254e609598f00360dcbb \\\n    --hash=sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193 \\\n    --hash=sha256:c6ccfab7baaf835fa90cb7ef3e9e7c240e84394420a5d33ba707f05318522fd6 \\\n    --hash=sha256:c6fc95b4f707efd506f3f7789140db9cec1b731999e7f033371bb6a5006a1ef8 \\\n    --hash=sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f \\\n    --hash=sha256:c9cc323768421f2dd2ab416e38c0302803771f73191ffa7134f00a4a5ca57e72 \\\n    --hash=sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b \\\n    --hash=sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2 \\\n    --hash=sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5 \\\n    --hash=sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c \\\n    --hash=sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032 \\\n    --hash=sha256:dbc6ee2241abe4f518685f603e427a94ceb73c08b6d15d85c6c5c4a71bde9c3e \\\n    --hash=sha256:de603df0d005177f7ef7faa56578d2d47fc93aaef165cdef91d64959176edb15 \\\n    --hash=sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be \\\n    --hash=sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621\nwerkzeug==1.0.1 \\\n    --hash=sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43 \\\n    --hash=sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c\n--extra-index-url https://test.pypi.org/simple\n"
  },
  {
    "path": "tests/fixtures/projects/demo-package/requirements_simple.txt",
    "content": "# This file is @generated by PDM.\n# Please do not edit it manually.\n\nclick==7.1.2\nflask==1.1.4\nitsdangerous==1.1.0\njinja2==2.11.3\nmarkupsafe==1.1.1\nwerkzeug==1.0.1\n--extra-index-url https://test.pypi.org/simple\n"
  },
  {
    "path": "tests/fixtures/projects/demo-package/setup.txt",
    "content": "\n# -*- coding: utf-8 -*-\nfrom setuptools import setup\n\nimport codecs\n\nwith codecs.open('README.md', encoding=\"utf-8\") as fp:\n    long_description = fp.read()\nINSTALL_REQUIRES = [\n    'flask',\n]\n\nsetup_kwargs = {\n    'name': 'my-package',\n    'version': '0.1.0',\n    'description': '',\n    'long_description': long_description,\n    'license': 'MIT',\n    'author': '',\n    'author_email': 'frostming <mianghong@gmail.com>',\n    'maintainer': None,\n    'maintainer_email': None,\n    'url': '',\n    'packages': [\n        'my_package',\n    ],\n    'package_data': {'': ['*']},\n    'long_description_content_type': 'text/markdown',\n    'install_requires': INSTALL_REQUIRES,\n    'python_requires': '>=3.5',\n\n}\n\n\nsetup(**setup_kwargs)\n"
  },
  {
    "path": "tests/fixtures/projects/demo-package/single_module.py",
    "content": "print(\"hello world\")\n"
  },
  {
    "path": "tests/fixtures/projects/demo-package-has-dep-with-extras/pyproject.toml",
    "content": "[project]\nname = \"\"\nversion = \"\"\ndescription = \"\"\nauthors = [\n    {name = \"Frost Ming\", email = \"mianghong@gmail.com\"},\n]\ndependencies = [\n    \"requests[security]~=2.26\",\n]\nrequires-python = \">=3.6\"\nlicense = {text = \"MIT\"}\n\n[build-system]\nrequires = [\"pdm-backend\"]\nbuild-backend = \"pdm.backend\"\n\n[tool]\n[tool.pdm]\n"
  },
  {
    "path": "tests/fixtures/projects/demo-package-has-dep-with-extras/requirements.txt",
    "content": "# This file is @generated by PDM.\n# Please do not edit it manually.\n\ncertifi==2021.10.8\ncharset-normalizer==2.0.7; python_version >= \"3\"\nidna==3.3; python_version >= \"3\"\nrequests==2.26.0\nurllib3==1.26.7\n"
  },
  {
    "path": "tests/fixtures/projects/demo-parent-package/README.md",
    "content": "# Package Package\n"
  },
  {
    "path": "tests/fixtures/projects/demo-parent-package/package-a/foo.py",
    "content": "__version__ = \"0.1.0\"\n"
  },
  {
    "path": "tests/fixtures/projects/demo-parent-package/package-a/setup.py",
    "content": "from setuptools import setup\n\n\nsetup(name=\"package-a\", py_modules=[\"foo\"], version=\"0.1.0\", install_requires=[\"flask\"])\n"
  },
  {
    "path": "tests/fixtures/projects/demo-parent-package/package-b/bar.py",
    "content": "__version__ = \"0.1.0\"\n"
  },
  {
    "path": "tests/fixtures/projects/demo-parent-package/package-b/pyproject.toml",
    "content": "[build-system]\nrequires = [\"pdm-backend\"]\nbuild-backend = \"pdm.backend\"\n\n[project]\nname = \"package-b\"\ndependencies = [\"django\"]\ndynamic = [\"version\"]\n\n[tool.pdm.version]\nsource = \"file\"\npath = \"bar.py\"\n"
  },
  {
    "path": "tests/fixtures/projects/demo-prerelease/demo.py",
    "content": "import os\n\nimport chardet\n\nprint(os.name)\n"
  },
  {
    "path": "tests/fixtures/projects/demo-prerelease/setup.py",
    "content": "from setuptools import setup\n\n\nsetup(\n    name=\"demo\",\n    version=\"0.0.2b0\",\n    description=\"test demo\",\n    py_modules=[\"demo\"],\n    python_requires=\">=3.3\",\n    install_requires=[\"idna\", \"chardet; os_name=='nt'\"],\n    extras_require={\n        \"tests\": [\"pytest\"],\n        \"security\": ['requests; python_version>=\"3.6\"'],\n    },\n)\n"
  },
  {
    "path": "tests/fixtures/projects/demo-src-package/LICENSE",
    "content": "MIT License\n"
  },
  {
    "path": "tests/fixtures/projects/demo-src-package/README.md",
    "content": "# This is a demo module\n"
  },
  {
    "path": "tests/fixtures/projects/demo-src-package/data_out.json",
    "content": "{\"name\": \"foo\"}\n"
  },
  {
    "path": "tests/fixtures/projects/demo-src-package/pyproject.toml",
    "content": "[build-system]\nrequires = [\"pdm-backend\"]\nbuild-backend = \"pdm.backend\"\n\n[project]\n# PEP 621 project metadata\n# See https://www.python.org/dev/peps/pep-0621/\nauthors = [\n    {name = \"frostming\", email = \"mianghong@gmail.com\"},\n]\ndynamic = [\"version\"]\nrequires-python = \">=3.5\"\nlicense = {text = \"MIT\"}\ndependencies = []\ndescription = \"\"\nname = \"demo-package\"\n\n[project.optional-dependencies]\n\n[tool.pdm.version]\nsource = \"file\"\npath = \"src/my_package/__init__.py\"\n"
  },
  {
    "path": "tests/fixtures/projects/demo-src-package/single_module.py",
    "content": "print(\"hello world\")\n"
  },
  {
    "path": "tests/fixtures/projects/demo-src-package/src/my_package/__init__.py",
    "content": "__version__ = \"0.1.0\"\n"
  },
  {
    "path": "tests/fixtures/projects/demo-src-package/src/my_package/data.json",
    "content": "{\"name\": \"demo-module\"}\n"
  },
  {
    "path": "tests/fixtures/projects/demo_extras/demo.py",
    "content": "import os\n\nprint(os.name)\n"
  },
  {
    "path": "tests/fixtures/projects/demo_extras/setup.py",
    "content": "from setuptools import setup\n\n\nsetup(\n    name=\"demo-extras\",\n    version=\"0.0.1\",\n    description=\"test demo\",\n    py_modules=[\"demo\"],\n    install_requires=[],\n    extras_require={\"extra1\": [\"requests[security]\"], \"extra2\": [\"requests[socks]\"]},\n)\n"
  },
  {
    "path": "tests/fixtures/projects/flit-demo/README.rst",
    "content": ""
  },
  {
    "path": "tests/fixtures/projects/flit-demo/doc/index.html",
    "content": ""
  },
  {
    "path": "tests/fixtures/projects/flit-demo/flit.py",
    "content": "\"\"\"An awesome flit demo\"\"\"\n__version__ = \"0.1.0\"\n"
  },
  {
    "path": "tests/fixtures/projects/flit-demo/pyproject.toml",
    "content": "[tool.flit.metadata]\nmodule=\"flit\"\nauthor=\"Thomas Kluyver\"\nauthor-email=\"thomas@kluyver.me.uk\"\nhome-page=\"https://github.com/takluyver/flit\"\nrequires = [\n    \"requests>=2.6\",\n    \"configparser; python_version == \\\"2.7\\\"\",\n]\ndist-name = \"pyflit\"\nrequires-python=\">=3.5\"\ndescription-file=\"README.rst\"\nclassifiers=[\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: BSD License\",\n    \"Programming Language :: Python :: 3\",\n    \"Topic :: Software Development :: Libraries :: Python Modules\",\n]\n\n[tool.flit.metadata.urls]\nDocumentation = \"https://flit.readthedocs.io/en/latest/\"\n\n[tool.flit.metadata.requires-extra]\ntest = [\n    \"pytest >=2.7.3\",\n    \"pytest-cov\",\n]\ndoc = [\"sphinx\"]\n\n[tool.flit.scripts]\nflit = \"flit:main\"\n\n[tool.flit.entrypoints.\"pygments.lexers\"]\ndogelang = \"dogelang.lexer:DogeLexer\"\n\n[tool.flit.sdist]\ninclude = [\"doc/\"]\nexclude = [\"doc/*.html\"]\n\n[build-system]\nrequires = [\"flit_core >=2,<4\"]\nbuild-backend = \"flit_core.buildapi\"\n"
  },
  {
    "path": "tests/fixtures/projects/poetry-demo/mylib.py",
    "content": "FOO = \"bar\"\n"
  },
  {
    "path": "tests/fixtures/projects/poetry-demo/pyproject.toml",
    "content": "[tool.poetry]\nname = \"poetry-demo\"\nversion = \"0.1.0\"\nauthors = [\"Thomas Kluyver <thomas@kluyver.me.uk>\"]\nhomepage = \"https://github.com/takluyver/flit\"\nlicense = \"BSD-3-Clause\"\ndescription = \"A demo project for Poetry\"\nclassifiers = [\n    \"Intended Audience :: Developers\",\n    \"Programming Language :: Python :: 3\",\n    \"Topic :: Software Development :: Libraries :: Python Modules\",\n]\n\npackages = [\n    { include = \"mylib.py\" },\n]\n\n[tool.poetry.urls]\nDocumentation = \"https://flit.readthedocs.io/en/latest/\"\n\n[tool.poetry.dependencies]\npython = \"^3.6\"\nrequests = \"^2.6\"\npytest = {version = \"^2.7.3\", optional = true}\npytest-cov = {version = \"*\", optional = true}\nsphinx = {version = \"*\", optional = true}\n\n[tool.poetry.extras]\ntest = [\"pytest\", \"pytest-cov\"]\ndoc = [\"sphinx\"]\n\n[build-system]\nrequires = [\"poetry-core\"]\nbuild-backend = \"poetry.core.masonry.api\"\n"
  },
  {
    "path": "tests/fixtures/projects/poetry-with-circular-dep/packages/child/child/__init__.py",
    "content": ""
  },
  {
    "path": "tests/fixtures/projects/poetry-with-circular-dep/packages/child/pyproject.toml",
    "content": "[tool.poetry]\nname = \"child\"\nversion = \"0.1.0\"\nauthors = [\"PDM <admin@pdm-project.org>\"]\ndescription = \"Child project\"\nlicense = \"Apache-2.0\"\npackages = [{include = \"child\"}]\n\n[tool.poetry.dependencies]\npython = \"^3.9.1\"\n\n[tool.poetry.group.dev.dependencies]\nparent = {path = \"../..\", develop = true}\n\n[build-system]\nrequires = [\"poetry-core\"]\nbuild-backend = \"poetry.core.masonry.api\"\n"
  },
  {
    "path": "tests/fixtures/projects/poetry-with-circular-dep/parent/__init__.py",
    "content": ""
  },
  {
    "path": "tests/fixtures/projects/poetry-with-circular-dep/pyproject.toml",
    "content": "[tool.poetry]\nname = \"parent\"\nversion = \"0.1.0\"\nauthors = [\"PDM <admin@pdm-project.org>\"]\ndescription = \"Parent project\"\nlicense = \"Apache-2.0\"\npackages = [{include = \"parent\"}]\n\n[tool.poetry.dependencies]\npython = \"^3.9.1\"\n\n[tool.poetry.group.dev.dependencies]\nchild = {path = \"./packages/child\", develop = true}\n\n[build-system]\nrequires = [\"poetry-core\"]\nbuild-backend = \"poetry.core.masonry.api\"\n"
  },
  {
    "path": "tests/fixtures/projects/test-hatch-static/README.md",
    "content": ""
  },
  {
    "path": "tests/fixtures/projects/test-hatch-static/pyproject.toml",
    "content": "[build-system]\nrequires = [\"hatchling>=0.15.0\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\nname = \"test-hatch\"\nversion = \"0.1.0\"\ndescription = \"Test hatch project\"\nreadme = \"README.md\"\nlicense = \"MIT\"\nrequires-python = \">=3.7\"\nauthors = [{ name = \"John\", email = \"john@example.org\" }]\nclassifiers = [\n  \"License :: OSI Approved :: MIT License\",\n]\ndependencies = [\"requests\", \"click\"]\n"
  },
  {
    "path": "tests/fixtures/projects/test-monorepo/README.md",
    "content": "# pdm_test\n"
  },
  {
    "path": "tests/fixtures/projects/test-monorepo/core/core.py",
    "content": ""
  },
  {
    "path": "tests/fixtures/projects/test-monorepo/core/pyproject.toml",
    "content": "[project]\nname = \"core\"\nversion = \"0.0.1\"\ndescription = \"\"\nrequires-python = \">= 3.7\"\ndependencies = []\n\n[build-system]\nrequires = [\"pdm-backend\"]\nbuild-backend = \"pdm.backend\"\n"
  },
  {
    "path": "tests/fixtures/projects/test-monorepo/package_a/alice.py",
    "content": ""
  },
  {
    "path": "tests/fixtures/projects/test-monorepo/package_a/pyproject.toml",
    "content": "[project]\nname = \"package_a\"\nversion = \"0.0.1\"\ndescription = \"\"\nrequires-python = \">= 3.7\"\ndependencies = [\n    \"core @ file:///${PROJECT_ROOT}/../core\",\n]\n\n[build-system]\nrequires = [\"pdm-backend\"]\nbuild-backend = \"pdm.backend\"\n"
  },
  {
    "path": "tests/fixtures/projects/test-monorepo/package_b/bob.py",
    "content": ""
  },
  {
    "path": "tests/fixtures/projects/test-monorepo/package_b/pyproject.toml",
    "content": "[project]\nname = \"package_b\"\nversion = \"0.0.1\"\ndescription = \"\"\nrequires-python = \">= 3.7\"\ndependencies = [\n    \"core @ file:///${PROJECT_ROOT}/../core\",\n]\n\n[build-system]\nrequires = [\"pdm-backend\"]\nbuild-backend = \"pdm.backend\"\n"
  },
  {
    "path": "tests/fixtures/projects/test-monorepo/pyproject.toml",
    "content": "[project]\nrequires-python = \">= 3.7\"\ndependencies = [\n    \"package_a @ file:///${PROJECT_ROOT}/package_a\",\n    \"package_b @ file:///${PROJECT_ROOT}/package_b\",\n]\n"
  },
  {
    "path": "tests/fixtures/projects/test-package-type-fixer/pyproject.toml",
    "content": "[build-system]\nrequires = [\"pdm-backend\"]\nbuild-backend = \"pdm.backend\"\n\n[project]\nversion = \"0.0.1\"\ndependencies = []\nname = \"test-package-type-fixer\"\nrequires-python = \">=3.9\"\n\n[dependency-groups]\ndev = [\n    \"requests==2.19.1\"\n]\n\n[tool.pdm.version]\nsource = \"file\"\npath = \"src/test_package_type_fixer/__init__.py\"\n\n[tool.pdm]\npackage-type = \"application\"\n"
  },
  {
    "path": "tests/fixtures/projects/test-package-type-fixer/src/test_package_type_fixer/__init__.py",
    "content": "__version__ = '0.1.0'"
  },
  {
    "path": "tests/fixtures/projects/test-plugin/hello.py",
    "content": "from pdm.cli.commands.base import BaseCommand\n\n\nclass HelloCommand(BaseCommand):\n    \"\"\"Say hello to somebody\"\"\"\n\n    def add_arguments(self, parser):\n        parser.add_argument(\"-n\", \"--name\", help=\"the person's name\")\n\n    def handle(self, project, options):\n        print(f\"Hello, {options.name or 'world'}\")\n\n\ndef main(core):\n    core.register_command(HelloCommand, \"hello\")\n"
  },
  {
    "path": "tests/fixtures/projects/test-plugin/setup.py",
    "content": "from setuptools import setup\n\n\nsetup(\n    name=\"test-plugin\",\n    version=\"0.0.1\",\n    py_modules=[\"hello\"],\n    entry_points={\"pdm.plugin\": [\"hello = hello:main\"]},\n)\n"
  },
  {
    "path": "tests/fixtures/projects/test-plugin-pdm/hello.py",
    "content": "from pdm.cli.commands.base import BaseCommand\n\n\nclass HelloCommand(BaseCommand):\n    \"\"\"Say hello to somebody\"\"\"\n\n    def add_arguments(self, parser):\n        parser.add_argument(\"-n\", \"--name\", help=\"the person's name\")\n\n    def handle(self, project, options):\n        print(f\"Hello, {options.name or 'world'}\")\n\n\ndef main(core):\n    core.register_command(HelloCommand, \"hello\")\n"
  },
  {
    "path": "tests/fixtures/projects/test-plugin-pdm/pyproject.toml",
    "content": "[build-system]\nrequires = [\"pdm-backend\"]\nbuild-backend = \"pdm.backend\"\n\n[project]\n# PEP 621 project metadata\n# See https://www.python.org/dev/peps/pep-0621/\nversion = \"0.0.1\"\ndependencies = []\nname = \"test-plugin-pdm\"\n\n[project.optional-dependencies]\n\n[project.entry-points.\"pdm.plugin\"]\nhello = \"hello:main\"\n"
  },
  {
    "path": "tests/fixtures/projects/test-removal/__init__.py",
    "content": ""
  },
  {
    "path": "tests/fixtures/projects/test-removal/bar.py",
    "content": ""
  },
  {
    "path": "tests/fixtures/projects/test-removal/foo.py",
    "content": ""
  },
  {
    "path": "tests/fixtures/projects/test-removal/subdir/__init__.py",
    "content": ""
  },
  {
    "path": "tests/fixtures/projects/test-setuptools/AUTHORS",
    "content": "frostming\n"
  },
  {
    "path": "tests/fixtures/projects/test-setuptools/README.md",
    "content": "# My Module\n"
  },
  {
    "path": "tests/fixtures/projects/test-setuptools/mymodule.py",
    "content": "__version__ = \"0.1.0\"\n"
  },
  {
    "path": "tests/fixtures/projects/test-setuptools/setup.cfg",
    "content": "[metadata]\nname = mymodule\ndescription = A test module\nkeywords = one, two\nclassifiers =\n    Framework :: Django\n    Programming Language :: Python :: 3\n\n[options]\nzip_safe = False\ninclude_package_data = True\npython_requires = >=3.5\npackage_dir = = src\ninstall_requires =\n    requests\n    importlib-metadata; python_version<\"3.10\"\n\n[options.entry_points]\nconsole_scripts =\n    mycli = mymodule:main\n"
  },
  {
    "path": "tests/fixtures/projects/test-setuptools/setup.py",
    "content": "from setuptools import setup\nfrom mymodule import __version__\n\nwith open(\"AUTHORS\") as f:\n    authors = f.read().strip()\n\nkwargs = {\n    \"name\": \"mymodule\",\n    \"version\": __version__,\n    \"author\": authors,\n}\n\nif 1 + 1 >= 2:\n    kwargs.update(license=\"MIT\")\n\n\nif __name__ == \"__main__\":\n    setup(**kwargs)\n"
  },
  {
    "path": "tests/fixtures/pypi.json",
    "content": "{\n  \"certifi\": {\n    \"2018.11.17\": {}\n  },\n  \"chardet\": {\n    \"3.0.4\": {}\n  },\n  \"demo\": {\n    \"0.0.1\": {\n      \"dependencies\": [\n        \"idna\",\n        \"chardet; os_name=='nt'\",\n        \"pytest; extra=='tests'\",\n        \"requests; python_version>='3.6' and extra=='security'\"\n      ],\n      \"requires_python\": \">=3.3\"\n    }\n  },\n  \"django\": {\n    \"1.11.8\": {\n      \"dependencies\": [\n        \"pytz\"\n      ]\n    },\n    \"2.2.9\": {\n      \"dependencies\": [\n        \"pytz\",\n        \"sqlparse\"\n      ],\n      \"requires_python\": \">=3.5\"\n    }\n  },\n  \"django-toolbar\": {\n    \"1.0\": {\n      \"dependencies\": [\n        \"django<2\"\n      ]\n    }\n  },\n  \"editables\": {\n    \"0.2\": {}\n  },\n  \"idna\": {\n    \"2.7\": {}\n  },\n  \"pyopenssl\": {\n    \"0.14\": {}\n  },\n  \"pysocks\": {\n    \"1.5.6\": {}\n  },\n  \"pytz\": {\n    \"2019.3\": {}\n  },\n  \"requests\": {\n    \"2.19.1\": {\n      \"dependencies\": [\n        \"certifi>=2017.4.17\",\n        \"chardet<3.1.0,>=3.0.2\",\n        \"idna<2.8,>=2.5\",\n        \"urllib3<1.24,>=1.21.1\",\n        \"PySocks>=1.5.6,!=1.5.7; extra=='socks'\",\n        \"pyOpenSSL>=0.14; extra=='security'\"\n      ]\n    },\n    \"2.20.0b1\": {\n      \"dependencies\": [\n        \"certifi>=2017.4.17\",\n        \"chardet<3.1.0,>=3.0.2\",\n        \"idna<2.8,>=2.5\",\n        \"urllib3<1.24,>=1.23b0\",\n        \"PySocks>=1.5.6,!=1.5.7; extra=='socks'\",\n        \"pyOpenSSL>=0.14; extra=='security'\"\n      ]\n    }\n  },\n  \"sqlparse\": {\n    \"0.3.0\": {\n      \"requires_python\": \">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*\"\n    }\n  },\n  \"urllib3\": {\n    \"1.22\": {},\n    \"1.23b0\": {}\n  },\n  \"using-demo\": {\n    \"0.1.0\": {\n      \"dependencies\": [\n        \"demo\"\n      ],\n      \"requires_python\": \">=3.3\"\n    }\n  }\n}"
  },
  {
    "path": "tests/fixtures/pyproject.toml",
    "content": "[tool.poetry]\nname = \"poetry\"\nversion = \"1.0.0\"\ndescription = \"Python dependency management and packaging made easy.\"\nauthors = [\n    \"Sébastien Eustace <sebastien@eustace.io>\",\n    \"Example, Inc. <inc@example.com>\"\n]\nlicense = \"MIT\"\n\nreadme = \"README.md\"\n\nhomepage = \"https://python-poetry.org/\"\nrepository = \"https://github.com/python-poetry/poetry\"\ndocumentation = \"https://python-poetry.org/docs\"\n\npackages = [\n    {include=\"my_package\", from=\"lib/\"},\n    {include=\"tests\", format=\"sdist\"}\n]\n\ninclude = [\"CHANGELOG.md\"]\nexclude = [\"my_package/excluded.py\"]\n\n[tool.poetry.dependencies]\npython = \"~2.7 || ^3.4\"\ncleo = { version = \"^0.7.6\", markers = \"python_version ~= '2.7'\" }\ncachecontrol = { version = \"^0.12.4\", extras = [\"filecache\"], python = \"^3.4\" }\nflask = { git = \"https://github.com/pallets/flask.git\", rev = \"38eb5d3b\" }\npsycopg2 = { version = \"^2.7\", optional = true }\nmysqlclient = { version = \"^1.3\", optional = true }\nbabel = \"2.9.0\"\n\n[tool.poetry.dev-dependencies]\ndemo-dir = { path = \"./projects/demo\" }\ndemo = { path = \"./artifacts/demo-0.0.1-py2.py3-none-any.whl\" }\n\n[tool.poetry.extras]\nmysql = [\"mysqlclient\"]\npgsql = [\"psycopg2\"]\n\n[tool.poetry.urls]\n\"Bug Tracker\" = \"https://github.com/python-poetry/poetry/issues\"\n\n[tool.poetry.plugins.\"blogtool.parsers\"]\n\".rst\" = \"some_module:SomeClass\"\n\n[tool.poetry.scripts]\npoetry = 'poetry.console:run'\n"
  },
  {
    "path": "tests/fixtures/requirements-include.txt",
    "content": "-r requirements.txt\n"
  },
  {
    "path": "tests/fixtures/requirements.txt",
    "content": "--index-url=https://pypi.org/simple\n--extra-index-url=https://pypi.example.com/simple\nwebassets==2.0\nwerkzeug==0.16.0\nwhoosh==2.7.4; sys_platform == \"win32\"\nwtforms==2.2.1 --hash=sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61 --hash=sha256:e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1\n-e git+https://github.com/pypa/pip.git@main#egg=pip\ngit+https://github.com/techalchemy/test-project.git@master#egg=pep508-package&subdirectory=parent_folder/pep508-package\n"
  },
  {
    "path": "tests/models/__init__.py",
    "content": ""
  },
  {
    "path": "tests/models/test_backends.py",
    "content": "import shutil\nfrom pathlib import Path\n\nimport pytest\n\nfrom pdm.models.backends import _BACKENDS, get_backend, get_relative_path\nfrom pdm.project import Project\nfrom pdm.utils import cd\nfrom tests import FIXTURES\n\n\ndef _setup_backend(project: Project, backend: str):\n    project.pyproject.metadata[\"requires-python\"] = \">=3.6\"\n    backend_cls = get_backend(backend)\n    project.pyproject.build_system.update(backend_cls.build_system())\n    project.root.joinpath(\"test_project\").mkdir()\n    project.root.joinpath(\"test_project/__init__.py\").touch()\n    if backend == \"setuptools\":\n        project.pyproject._data.setdefault(\"tool\", {}).setdefault(\"setuptools\", {}).update(packages=[\"test_project\"])\n    elif backend == \"hatchling\":\n        project.pyproject._data.setdefault(\"tool\", {}).setdefault(\"hatch\", {}).setdefault(\"metadata\", {}).update(\n            {\"allow-direct-references\": True}\n        )\n    project.pyproject.write()\n    project._environment = None\n    assert isinstance(project.backend, backend_cls)\n\n\n@pytest.mark.parametrize(\"backend\", _BACKENDS.keys())\ndef test_project_backend(project, working_set, backend, pdm):\n    _setup_backend(project, backend)\n    shutil.copytree(FIXTURES / \"projects/demo\", project.root / \"demo\")\n    project.root.joinpath(\"sub\").mkdir()\n    with cd(project.root.joinpath(\"sub\")):\n        pdm([\"add\", \"--no-self\", \"../demo\"], obj=project, strict=True)\n        assert \"idna\" in working_set\n        assert \"demo\" in working_set\n        dep = project.pyproject.metadata[\"dependencies\"][0]\n        demo_path = project.root.joinpath(\"demo\")\n        demo_url = demo_path.as_uri()\n        if backend == \"pdm-backend\":\n            assert dep == \"demo @ file:///${PROJECT_ROOT}/demo\"\n        elif backend == \"hatchling\":\n            assert dep == \"demo @ {root:uri}/demo\"\n        else:\n            assert dep == f\"demo @ {demo_url}\"\n        assert project.backend.expand_line(dep) == f\"demo @ {demo_url}\"\n        if backend not in (\"hatchling\", \"pdm-backend\"):\n            candidate = project.make_self_candidate()\n            # We skip hatchling here to avoid installing hatchling into the build env\n            metadata_dependency = candidate.prepare(project.environment).metadata.requires[0]\n            assert metadata_dependency == f\"demo @ {demo_url}\"\n\n\ndef test_hatch_expand_variables(monkeypatch):\n    root = Path().absolute()\n    root_url = root.as_uri()\n    backend = get_backend(\"hatchling\")(root)\n    monkeypatch.setenv(\"BAR\", \"bar\")\n    assert backend.expand_line(\"demo @ {root:uri}/demo\") == f\"demo @ {root_url}/demo\"\n    assert backend.expand_line(\"demo=={env:FOO:{env:BAR}}\") == \"demo==bar\"\n    assert backend.relative_path_to_url(\"demo package\") == \"{root:uri}/demo%20package\"\n    assert backend.relative_path_to_url(\"../demo\") == \"{root:uri}/../demo\"\n\n\ndef test_pdm_backend_expand_variables(monkeypatch):\n    root = Path().absolute()\n    root_url = root.as_uri()\n    backend = get_backend(\"pdm-backend\")(root)\n    monkeypatch.setenv(\"BAR\", \"bar\")\n    assert backend.expand_line(\"demo @ file:///${PROJECT_ROOT}/demo\") == f\"demo @ {root_url}/demo\"\n    assert backend.expand_line(\"demo==${BAR}\") == \"demo==bar\"\n    assert backend.relative_path_to_url(\"demo package\") == \"file:///${PROJECT_ROOT}/demo%20package\"\n    assert backend.relative_path_to_url(\"../demo\") == \"file:///${PROJECT_ROOT}/../demo\"\n\n\n@pytest.mark.parametrize(\n    \"url,path\",\n    [\n        (\"file:///foo/bar\", None),\n        (\"https://example.org\", None),\n        (\"file:///${PROJECT_ROOT}/demo%20package\", \"demo package\"),\n        (\"file:///${PROJECT_ROOT}/../demo\", \"../demo\"),\n        (\"{root:uri}/demo%20package\", \"demo package\"),\n    ],\n)\ndef test_get_relative_path(url, path):\n    assert get_relative_path(url) == path\n"
  },
  {
    "path": "tests/models/test_candidates.py",
    "content": "from __future__ import annotations\n\nimport shutil\n\nimport pytest\nfrom unearth import Link\n\nfrom pdm.exceptions import RequirementError\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.requirements import Requirement, parse_requirement\nfrom pdm.models.specifiers import PySpecSet\nfrom pdm.utils import is_path_relative_to\nfrom tests import FIXTURES\n\n\ndef to_lines(requirements: list[Requirement]) -> list[str]:\n    return sorted([r.as_line() for r in requirements])\n\n\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_parse_local_directory_metadata(project, is_editable):\n    requirement_line = f\"{(FIXTURES / 'projects/demo').as_posix()}\"\n    req = parse_requirement(requirement_line, is_editable)\n    candidate = Candidate(req)\n    assert to_lines(candidate.prepare(project.environment).get_dependencies_from_metadata()) == [\n        'chardet; os_name == \"nt\"',\n        \"idna\",\n    ]\n    assert candidate.name == \"demo\"\n    assert candidate.version == \"0.0.1\"\n\n\n@pytest.mark.usefixtures(\"vcs\", \"local_finder\")\ndef test_parse_vcs_metadata(project, is_editable):\n    requirement_line = \"git+https://github.com/test-root/demo.git@master#egg=demo\"\n    req = parse_requirement(requirement_line, is_editable)\n    candidate = Candidate(req)\n    assert to_lines(candidate.prepare(project.environment).get_dependencies_from_metadata()) == [\n        'chardet; os_name == \"nt\"',\n        \"idna\",\n    ]\n    assert candidate.name == \"demo\"\n    assert candidate.version == \"0.0.1\"\n    lockfile = candidate.as_lockfile_entry(project.root)\n    assert lockfile[\"ref\"] == \"master\"\n    if is_editable:\n        assert \"revision\" not in lockfile\n    else:\n        assert lockfile[\"revision\"] == \"1234567890abcdef\"\n\n\n@pytest.mark.usefixtures(\"local_finder\")\n@pytest.mark.parametrize(\n    \"requirement_line\",\n    [\n        f\"{(FIXTURES / 'artifacts/demo-0.0.1.tar.gz').as_posix()}\",\n        f\"{(FIXTURES / 'artifacts/demo-0.0.1-py2.py3-none-any.whl').as_posix()}\",\n    ],\n)\ndef test_parse_artifact_metadata(requirement_line, project):\n    req = parse_requirement(requirement_line)\n    candidate = Candidate(req)\n    assert to_lines(candidate.prepare(project.environment).get_dependencies_from_metadata()) == [\n        'chardet; os_name == \"nt\"',\n        \"idna\",\n    ]\n    assert candidate.name == \"demo\"\n    assert candidate.version == \"0.0.1\"\n\n\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_parse_metadata_with_extras(project):\n    req = parse_requirement(\n        f\"demo[tests,security] @ {(FIXTURES / 'artifacts/demo-0.0.1-py2.py3-none-any.whl').as_uri()}\"\n    )\n    candidate = Candidate(req)\n    prepared = candidate.prepare(project.environment)\n    assert prepared.link.is_wheel\n    assert to_lines(prepared.get_dependencies_from_metadata()) == [\n        \"pytest\",\n        'requests; python_version >= \"3.6\"',\n    ]\n\n\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_parse_remote_link_metadata(project):\n    req = parse_requirement(\"http://fixtures.test/artifacts/demo-0.0.1-py2.py3-none-any.whl\")\n    candidate = Candidate(req)\n    prepared = candidate.prepare(project.environment)\n    assert prepared.link.is_wheel\n    assert to_lines(prepared.get_dependencies_from_metadata()) == [\n        'chardet; os_name == \"nt\"',\n        \"idna\",\n    ]\n    assert candidate.name == \"demo\"\n    assert candidate.version == \"0.0.1\"\n\n\n@pytest.mark.usefixtures(\"local_finder\")\n@pytest.mark.parametrize(\n    \"req_str\",\n    [\n        \"demo @ file:///${PROJECT_ROOT}/tests/fixtures/artifacts/demo-0.0.1-py2.py3-none-any.whl\",\n        \"demo @ file:///${PROJECT_ROOT}/tests/fixtures/artifacts/demo-0.0.1.tar.gz\",\n        \"demo @ file:///${PROJECT_ROOT}/tests/fixtures/projects/demo\",\n        \"-e ./tests/fixtures/projects/demo\",\n        \"-e file:///${PROJECT_ROOT}/tests/fixtures/projects/demo#egg=demo\",\n        \"-e file:///${PROJECT_ROOT}/tests/fixtures/projects/demo#-with-hash#egg=demo\",\n    ],\n)\ndef test_expand_project_root_in_url(req_str, core):\n    project = core.create_project(FIXTURES.parent.parent)\n    if req_str.startswith(\"-e \"):\n        req = parse_requirement(req_str[3:], True)\n    else:\n        req = parse_requirement(req_str)\n    req.relocate(project.backend)\n    candidate = Candidate(req)\n    assert to_lines(candidate.prepare(project.environment).get_dependencies_from_metadata()) == [\n        'chardet; os_name == \"nt\"',\n        \"idna\",\n    ]\n    lockfile_entry = candidate.as_lockfile_entry(project.root)\n    if \"path\" in lockfile_entry:\n        assert lockfile_entry[\"path\"].startswith(\"./\")\n    else:\n        assert \"${PROJECT_ROOT}\" in lockfile_entry[\"url\"]\n\n\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_parse_project_file_on_build_error(project):\n    req = parse_requirement(f\"{(FIXTURES / 'projects/demo-failure').as_posix()}\")\n    candidate = Candidate(req)\n    assert to_lines(candidate.prepare(project.environment).get_dependencies_from_metadata()) == [\n        'chardet; os_name == \"nt\"',\n        \"idna\",\n    ]\n    assert candidate.name == \"demo\"\n    assert candidate.version == \"0.0.1\"\n\n\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_parse_project_file_on_build_error_with_extras(project):\n    req = parse_requirement(f\"{(FIXTURES / 'projects/demo-failure').as_posix()}\")\n    req.extras = (\"security\", \"tests\")\n    candidate = Candidate(req)\n    deps = to_lines(candidate.prepare(project.environment).get_dependencies_from_metadata())\n    assert 'requests; python_version >= \"3.6\"' in deps\n    assert \"pytest\" in deps\n    assert candidate.name == \"demo\"\n    assert candidate.version == \"0.0.1\"\n\n\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_parse_project_file_on_build_error_no_dep(project):\n    req = parse_requirement(f\"{(FIXTURES / 'projects/demo-failure-no-dep').as_posix()}\")\n    candidate = Candidate(req)\n    assert to_lines(candidate.prepare(project.environment).get_dependencies_from_metadata()) == []\n    assert candidate.name == \"demo\"\n    assert candidate.version == \"0.0.1\"\n\n\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_parse_poetry_project_metadata(project, is_editable):\n    req = parse_requirement(f\"{(FIXTURES / 'projects/poetry-demo').as_posix()}\", is_editable)\n    candidate = Candidate(req)\n    requests_dep = \"requests<3.0,>=2.6\"\n    assert to_lines(candidate.prepare(project.environment).get_dependencies_from_metadata()) == [requests_dep]\n    assert candidate.name == \"poetry-demo\"\n    assert candidate.version == \"0.1.0\"\n\n\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_parse_flit_project_metadata(project, is_editable):\n    req = parse_requirement(f\"{(FIXTURES / 'projects/flit-demo').as_posix()}\", is_editable)\n    candidate = Candidate(req)\n    deps = to_lines(candidate.prepare(project.environment).get_dependencies_from_metadata())\n    requests_dep = \"requests>=2.6\"\n    assert requests_dep in deps\n    assert 'configparser; python_version == \"2.7\"' in deps\n    assert candidate.name == \"pyflit\"\n    assert candidate.version == \"0.1.0\"\n\n\n@pytest.mark.usefixtures(\"vcs\", \"local_finder\")\ndef test_vcs_candidate_in_subdirectory(project, is_editable):\n    line = \"git+https://github.com/test-root/demo-parent-package.git@master#egg=package-a&subdirectory=package-a\"\n    req = parse_requirement(line, is_editable)\n    candidate = Candidate(req)\n    assert to_lines(candidate.prepare(project.environment).get_dependencies_from_metadata()) == [\"flask\"]\n    assert candidate.version == \"0.1.0\"\n\n    line = \"git+https://github.com/test-root/demo-parent-package.git@master#egg=package-b&subdirectory=package-b\"\n    req = parse_requirement(line, is_editable)\n    candidate = Candidate(req)\n    expected_deps = [\"django\"]\n    assert to_lines(candidate.prepare(project.environment).get_dependencies_from_metadata()) == expected_deps\n    tail = \"+editable\" if is_editable else \"\"\n    assert candidate.version == f\"0.1.0{tail}\"\n\n\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_sdist_candidate_with_wheel_cache(project, mocker):\n    file_link = Link((FIXTURES / \"artifacts/demo-0.0.1.tar.gz\").as_uri())\n    built_path = FIXTURES / \"artifacts/demo-0.0.1-py2.py3-none-any.whl\"\n    wheel_cache = project.make_wheel_cache()\n    cache_path = wheel_cache.get_path_for_link(file_link, project.environment.spec)\n    if not cache_path.exists():\n        cache_path.mkdir(parents=True)\n    shutil.copy2(built_path, cache_path)\n    req = parse_requirement(file_link.url)\n    downloader = mocker.patch(\"unearth.finder.unpack_link\")\n    prepared = Candidate(req).prepare(project.environment)\n    prepared.metadata\n    downloader.assert_not_called()\n    assert prepared._cached.name == built_path.name\n\n    prepared._cached = None\n    builder = mocker.patch(\"pdm.builders.WheelBuilder.build\")\n    wheel = prepared.build()\n    builder.assert_not_called()\n    assert wheel.name == built_path.name\n\n\n@pytest.mark.usefixtures(\"vcs\", \"local_finder\")\ndef test_cache_vcs_immutable_revision(project):\n    req = parse_requirement(\"git+https://github.com/test-root/demo.git@master#egg=demo\")\n    candidate = Candidate(req)\n    candidate.prepare(project.environment).build()\n    assert not is_path_relative_to(candidate.prepared._get_build_cache(), project.cache_dir)\n    assert candidate.get_revision() == \"1234567890abcdef\"\n\n    req = parse_requirement(\"git+https://github.com/test-root/demo.git@1234567890abcdef#egg=demo\")\n    candidate = Candidate(req)\n    candidate.prepare(project.environment).build()\n    assert is_path_relative_to(candidate.prepared._get_build_cache(), project.cache_dir)\n    assert candidate.get_revision() == \"1234567890abcdef\"\n\n    # test the revision can be got correctly after cached\n    prepared = Candidate(req).prepare(project.environment)\n    assert not prepared._source_dir\n    assert prepared.revision == \"1234567890abcdef\"\n\n\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_cache_egg_info_sdist(project):\n    req = parse_requirement(\"demo @ http://fixtures.test/artifacts/demo-0.0.1.tar.gz\")\n    candidate = Candidate(req)\n    candidate.prepare(project.environment).build()\n    assert is_path_relative_to(candidate.prepared._get_build_cache(), project.cache_dir)\n\n\ndef test_invalidate_incompatible_wheel_link(project):\n    project.project_config[\"pypi.url\"] = \"https://my.pypi.org/simple\"\n    project.environment.python_requires = PySpecSet(\">=3.6\")\n    project.environment.__dict__[\"spec\"] = project.environment.spec.replace(platform=\"linux\")\n    req = parse_requirement(\"demo\")\n    prepared = Candidate(\n        req,\n        name=\"demo\",\n        version=\"0.0.1\",\n        link=Link(\"http://fixtures.test/artifacts/demo-0.0.1-cp36-cp36m-win_amd64.whl\"),\n    ).prepare(project.environment)\n    prepared._obtain(True)\n    assert prepared._cached.name == prepared.link.filename == \"demo-0.0.1-cp36-cp36m-win_amd64.whl\"\n\n    prepared._obtain(False)\n    assert prepared._cached.name == prepared.link.filename == \"demo-0.0.1-py2.py3-none-any.whl\"\n\n\ndef test_legacy_pep345_tag_link(project):\n    project.project_config[\"pypi.url\"] = \"https://my.pypi.org/simple\"\n    req = parse_requirement(\"pep345-legacy\")\n    repo = project.get_repository()\n    _ = next(iter(repo.find_candidates(req)))\n\n\n@pytest.mark.filterwarnings(\"ignore::FutureWarning\")\ndef test_ignore_invalid_py_version(project):\n    project.project_config[\"pypi.url\"] = \"https://my.pypi.org/simple\"\n    req = parse_requirement(\"wheel\")\n    repo = project.get_repository()\n    _ = next(iter(repo.find_candidates(req)))\n\n\ndef test_find_candidates_from_find_links(project):\n    project.pyproject.settings[\"source\"] = [\n        {\"name\": \"pypi\", \"url\": \"http://fixtures.test/index/demo.html\", \"verify_ssl\": False, \"type\": \"find_links\"}\n    ]\n    project.pyproject.write(False)\n    repo = project.get_repository()\n    candidates = list(repo.find_candidates(parse_requirement(\"demo\")))\n    assert len(candidates) == 1\n    assert candidates[0].link.filename == \"demo-0.0.1-py2.py3-none-any.whl\"\n\n\ndef test_parse_metadata_from_pep621(project, mocker):\n    builder = mocker.patch(\"pdm.builders.wheel.WheelBuilder.build\")\n    req = parse_requirement(f\"test-hatch @ {(FIXTURES / 'projects/test-hatch-static').as_uri()}\")\n    candidate = Candidate(req)\n    distribution = candidate.prepare(project.environment).metadata\n    assert sorted(distribution.requires) == [\"click\", \"requests\"]\n    assert distribution.metadata[\"Summary\"] == \"Test hatch project\"\n    builder.assert_not_called()\n\n\ndef test_parse_metadata_with_dynamic_fields(project, local_finder):\n    req = parse_requirement(f\"demo-package @ {(FIXTURES / 'projects/demo-src-package').as_uri()}\")\n    candidate = Candidate(req)\n    metadata = candidate.prepare(project.environment).metadata\n    assert not metadata.requires\n    assert metadata.version == \"0.1.0\"\n\n\ndef test_get_metadata_for_non_existing_path(project):\n    req = parse_requirement(\"file:///${PROJECT_ROOT}/non-existing-path\")\n    candidate = Candidate(req)\n    with pytest.raises(RequirementError, match=r\"The local path '.+' does not exist\"):\n        candidate.prepare(project.environment).metadata\n"
  },
  {
    "path": "tests/models/test_marker.py",
    "content": "import pytest\n\nfrom pdm.models.markers import EnvSpec, get_marker\nfrom pdm.models.specifiers import PySpecSet\n\n\n@pytest.mark.parametrize(\n    \"original,marker,py_spec\",\n    [\n        (\"python_version > '3'\", \"\", \">=3.1\"),\n        (\"python_version > '3.8'\", \"\", \">=3.9\"),\n        (\"python_version != '3.8'\", \"\", \"!=3.8.*\"),\n        (\"python_version == '3.7'\", \"\", \"==3.7.*\"),\n        (\"python_version in '3.6 3.7'\", \"\", \">=3.6.0,<3.8.0\"),\n        (\"python_full_version >= '3.6.0'\", \"\", \">=3.6\"),\n        (\"python_full_version not in '3.8.3'\", \"\", \"!=3.8.3\"),\n        # mixed marker and python version\n        (\"python_version > '3.7' and os_name == 'nt'\", 'os_name == \"nt\"', \">=3.8\"),\n        (\n            \"python_version > '3.7' or os_name == 'nt'\",\n            'python_version > \"3.7\" or os_name == \"nt\"',\n            \"\",\n        ),\n    ],\n)\ndef test_split_pyspec(original, marker, py_spec):\n    m = get_marker(original)\n    a, b = m.split_pyspec()\n    assert marker == str(a)\n    assert b == PySpecSet(py_spec)\n\n\n@pytest.mark.parametrize(\n    \"marker,env_spec,expected\",\n    [\n        (\"os_name == 'nt'\", EnvSpec.from_spec(\">=3.10\", \"windows\"), True),\n        (\"os_name == 'nt'\", EnvSpec.from_spec(\">=3.10\"), True),\n        (\"os_name != 'nt'\", EnvSpec.from_spec(\">=3.10\", \"windows\"), False),\n        (\"python_version >= '3.7' and os_name == 'nt'\", EnvSpec.from_spec(\">=3.10\"), True),\n        (\"python_version < '3.7' and os_name == 'nt'\", EnvSpec.from_spec(\">=3.10\"), False),\n        (\"python_version < '3.7' or os_name == 'nt'\", EnvSpec.from_spec(\">=3.10\"), False),\n        (\"python_version >= '3.7' and os_name == 'nt'\", EnvSpec.from_spec(\">=3.10\", \"linux\"), False),\n        (\"python_version >= '3.7' or os_name == 'nt'\", EnvSpec.from_spec(\">=3.10\", \"linux\"), True),\n        (\"python_version >= '3.7' and implementation_name == 'pypy'\", EnvSpec.from_spec(\">=3.10\"), True),\n        (\n            \"python_version >= '3.7' and implementation_name == 'pypy'\",\n            EnvSpec.from_spec(\">=3.10\", implementation=\"cpython\"),\n            False,\n        ),\n    ],\n)\ndef test_match_env_spec(marker, env_spec, expected):\n    m = get_marker(marker)\n    assert m.matches(env_spec) is expected\n"
  },
  {
    "path": "tests/models/test_requirements.py",
    "content": "from __future__ import annotations\n\nimport os\n\nimport pytest\n\nfrom pdm.models.requirements import RequirementError, filter_requirements_with_extras, parse_requirement\nfrom tests import FIXTURES\n\nFILE_PREFIX = \"file:///\" if os.name == \"nt\" else \"file://\"\n\nREQUIREMENTS = [\n    (\"requests\", None),\n    (\"requests<2.21.0,>=2.20.0\", None),\n    (\n        'requests==2.19.0; os_name == \"nt\"',\n        None,\n    ),\n    (\n        'requests[security,tests]==2.8.*,>=2.8.1; python_version < \"2.7\"',\n        None,\n    ),\n    (\n        'pip @ https://github.com/pypa/pip/archive/1.3.1.zip ; python_version > \"3.4\"',\n        None,\n    ),\n    (\n        \"git+http://git.example.com/MyProject.git@master#egg=MyProject\",\n        \"MyProject @ git+http://git.example.com/MyProject.git@master\",\n    ),\n    (\n        \"https://github.com/pypa/pip/archive/1.3.1.zip\",\n        None,\n    ),\n    (\n        (FIXTURES / \"projects/demo\").as_posix(),\n        \"demo @ \" + (FIXTURES / \"projects/demo\").as_uri(),\n    ),\n    (\n        (FIXTURES / \"artifacts/demo-0.0.1-py2.py3-none-any.whl\").as_posix(),\n        \"demo @ \" + (FIXTURES / \"artifacts/demo-0.0.1-py2.py3-none-any.whl\").as_uri(),\n    ),\n    (\n        (FIXTURES / \"projects/demo\").as_posix() + \"[security]\",\n        \"demo[security] @ \" + (FIXTURES / \"projects/demo\").as_uri(),\n    ),\n    (\n        'requests; python_version==\"3.7.*\"',\n        'requests; python_version == \"3.7.*\"',\n    ),\n    (\n        \"git+git@github.com:pypa/pip.git#egg=pip\",\n        \"pip @ git+ssh://git@github.com/pypa/pip.git\",\n    ),\n    pytest.param(\n        \"foo >=4.*, <=5.*\",\n        \"foo<5.0,>=4.0\",\n    ),\n    pytest.param(\n        \"foo (>=4.*, <=5.*)\",\n        \"foo<5.0,>=4.0\",\n    ),\n    pytest.param(\n        \"foo>=3.0+g1234; python_version>='3.6'\",\n        'foo>=3.0; python_version >= \"3.6\"',\n    ),\n]\n\n\ndef filter_requirements_to_lines(\n    requirements: list[str], extras: tuple[str, ...], include_default: bool = False\n) -> list[str]:\n    return [\n        req.as_line() for req in filter_requirements_with_extras(requirements, extras, include_default=include_default)\n    ]\n\n\n@pytest.mark.filterwarnings(\"ignore::FutureWarning\")\n@pytest.mark.parametrize(\"req, result\", REQUIREMENTS)\ndef test_convert_req_dict_to_req_line(req, result):\n    r = parse_requirement(req)\n    result = result or req\n    assert r.as_line() == result\n\n\n@pytest.mark.parametrize(\n    \"line,expected\",\n    [\n        (\"requests; os_name=>'nt'\", None),\n        (\"django>=2<4\", None),\n    ],\n)\ndef test_illegal_requirement_line(line, expected):\n    with pytest.raises(RequirementError, match=expected):\n        parse_requirement(line)\n\n\n@pytest.mark.parametrize(\"line\", [\"requests >= 2.19.0\", \"https://github.com/pypa/pip/archive/1.3.1.zip\"])\ndef test_not_supported_editable_requirement(line):\n    with pytest.raises(RequirementError, match=\"Editable requirement is only supported\"):\n        parse_requirement(line, True)\n\n\ndef test_filter_requirements_with_extras():\n    requirements = [\n        \"foo; extra == 'a'\",\n        \"bar; extra == 'b'\",\n        \"baz; extra == 'a' or extra == 'b'\",\n        \"qux; extra == 'a' and extra == 'b'\",\n        \"ping; os_name == 'nt' and extra == 'a'\",\n        \"blah\",\n    ]\n    assert filter_requirements_to_lines(requirements, ()) == [\"blah\"]\n    assert filter_requirements_to_lines(requirements, (\"a\",)) == [\"foo\", \"baz\", 'ping; os_name == \"nt\"']\n    assert filter_requirements_to_lines(requirements, (\"b\",)) == [\"bar\", \"baz\"]\n    assert filter_requirements_to_lines(requirements, (\"a\", \"b\")) == [\n        \"foo\",\n        \"bar\",\n        \"baz\",\n        \"qux\",\n        'ping; os_name == \"nt\"',\n    ]\n    assert filter_requirements_to_lines(requirements, (\"c\",)) == []\n    assert filter_requirements_to_lines(requirements, (\"a\", \"b\"), include_default=True) == [\n        \"foo\",\n        \"bar\",\n        \"baz\",\n        \"qux\",\n        'ping; os_name == \"nt\"',\n        \"blah\",\n    ]\n"
  },
  {
    "path": "tests/models/test_session.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from pdm.project.core import Project\n\n\ndef test_session_sources_all_proxy(project: Project, mocker, monkeypatch):\n    monkeypatch.setenv(\"all_proxy\", \"http://localhost:8888\")\n    mock_get_transport = mocker.patch(\"pdm.models.session._get_transport\")\n\n    assert project.environment.session is not None\n    transport_args = mock_get_transport.call_args\n    assert transport_args is not None\n    assert transport_args.kwargs[\"proxy\"].url == \"http://localhost:8888\"\n\n    monkeypatch.setenv(\"no_proxy\", \"pypi.org\")\n    mock_get_transport.reset_mock()\n    del project.environment.session\n    assert project.environment.session is not None\n    transport_args = mock_get_transport.call_args\n    assert transport_args is not None\n    assert transport_args.kwargs[\"proxy\"] is None\n"
  },
  {
    "path": "tests/models/test_setup_parsing.py",
    "content": "import pytest\n\nfrom pdm.models.setup import Setup\n\n\n@pytest.mark.parametrize(\n    \"content, result\",\n    [\n        (\n            \"\"\"[metadata]\nname = foo\nversion = 0.1.0\n\"\"\",\n            Setup(\"foo\", \"0.1.0\"),\n        ),\n        (\n            \"\"\"[metadata]\nname = foo\nversion = attr:foo.__version__\n\"\"\",\n            Setup(\"foo\", \"0.0.0\"),\n        ),\n        (\n            \"\"\"[metadata]\nname = foo\nversion = 0.1.0\n\n[options]\npython_requires = >=3.6\ninstall_requires =\n    click\n    requests\n[options.extras_require]\ntui =\n    rich\n\"\"\",\n            Setup(\"foo\", \"0.1.0\", [\"click\", \"requests\"], {\"tui\": [\"rich\"]}, \">=3.6\"),\n        ),\n    ],\n)\ndef test_parse_setup_cfg(content, result, tmp_path):\n    tmp_path.joinpath(\"setup.cfg\").write_text(content)\n    assert Setup.from_directory(tmp_path) == result\n\n\n@pytest.mark.parametrize(\n    \"content,result\",\n    [\n        (\n            \"\"\"from setuptools import setup\n\nsetup(name=\"foo\", version=\"0.1.0\")\n\"\"\",\n            Setup(\"foo\", \"0.1.0\"),\n        ),\n        (\n            \"\"\"import setuptools\n\nsetuptools.setup(name=\"foo\", version=\"0.1.0\")\n\"\"\",\n            Setup(\"foo\", \"0.1.0\"),\n        ),\n        (\n            \"\"\"from setuptools import setup\n\nkwargs = {\"name\": \"foo\", \"version\": \"0.1.0\"}\nsetup(**kwargs)\n\"\"\",\n            Setup(\"foo\", \"0.1.0\"),\n        ),\n        (\n            \"\"\"from setuptools import setup\nname = 'foo'\nsetup(name=name, version=\"0.1.0\")\n\"\"\",\n            Setup(\"foo\", \"0.1.0\"),\n        ),\n        (\n            \"\"\"from setuptools import setup\n\nsetup(name=\"foo\", version=\"0.1.0\", install_requires=['click', 'requests'],\n      python_requires='>=3.6', extras_require={'tui': ['rich']})\n\"\"\",\n            Setup(\"foo\", \"0.1.0\", [\"click\", \"requests\"], {\"tui\": [\"rich\"]}, \">=3.6\"),\n        ),\n        (\n            \"\"\"from pathlib import Path\nfrom setuptools import setup\n\nversion = Path('__version__.py').read_text().strip()\n\nsetup(name=\"foo\", version=version)\n\"\"\",\n            Setup(\"foo\", \"0.0.0\"),\n        ),\n    ],\n)\ndef test_parse_setup_py(content, result, tmp_path):\n    tmp_path.joinpath(\"setup.py\").write_text(content)\n    assert Setup.from_directory(tmp_path) == result\n\n\ndef test_parse_pyproject_toml(tmp_path):\n    content = \"\"\"[project]\nname = \"foo\"\nversion = \"0.1.0\"\nrequires-python = \">=3.6\"\ndependencies = [\"click\", \"requests\"]\n\n[project.optional-dependencies]\ntui = [\"rich\"]\n\"\"\"\n    tmp_path.joinpath(\"pyproject.toml\").write_text(content)\n    result = Setup(\"foo\", \"0.1.0\", [\"click\", \"requests\"], {\"tui\": [\"rich\"]}, \">=3.6\")\n    assert Setup.from_directory(tmp_path) == result\n"
  },
  {
    "path": "tests/models/test_setup_parsing_extra.py",
    "content": "import logging\nimport textwrap\n\nimport pytest\n\nfrom pdm.exceptions import ProjectError\nfrom pdm.formats import MetaConvertError\nfrom pdm.models.setup import Setup\n\n\ndef test_setup_update_truthiness_semantics():\n    base = Setup(name=\"foo\", install_requires=[\"a\"], summary=None)\n    other = Setup(name=None, install_requires=[], summary=\"some desc\")\n    base.update(other)\n    assert base.name == \"foo\"  # not overridden by falsy\n    assert base.install_requires == [\"a\"]  # not overridden by empty list\n    assert base.summary == \"some desc\"  # overridden by truthy\n\n\ndef test_parse_setup_py_with_kwargs_dict_and_variables(tmp_path):\n    content = textwrap.dedent(\n        \"\"\"\n        from setuptools import setup\n        reqs = ['click', 'requests']\n        extras_list = ['rich']\n        extras = {'tui': extras_list}\n        kwargs = dict(name='foo', version='0.1.0', install_requires=reqs,\n                      python_requires='>=3.8', extras_require=extras)\n        setup(**kwargs)\n        \"\"\"\n    )\n    tmp_path.joinpath(\"setup.py\").write_text(content)\n    assert Setup.from_directory(tmp_path) == Setup(\n        name=\"foo\",\n        version=\"0.1.0\",\n        install_requires=[\"click\", \"requests\"],\n        extras_require={\"tui\": [\"rich\"]},\n        python_requires=\">=3.8\",\n    )\n\n\ndef test_setup_as_dict():\n    s = Setup(\n        name=\"n\", version=\"1\", install_requires=[\"a\"], extras_require={\"x\": [\"b\"]}, python_requires=\">=3.8\", summary=\"d\"\n    )\n    d = s.as_dict()\n    assert d[\"name\"] == \"n\"\n    assert d[\"version\"] == \"1\"\n    assert d[\"install_requires\"] == [\"a\"]\n    assert d[\"extras_require\"] == {\"x\": [\"b\"]}\n    assert d[\"python_requires\"] == \">=3.8\"\n    assert d[\"summary\"] == \"d\"\n\n\n@pytest.mark.parametrize(\n    \"content\",\n    [\n        # Last element is an if but not a Compare\n        \"\"\"\nif True:\n    pass\n\"\"\",\n        # Compare but left is Attribute not Name\n        \"\"\"\nimport pkg\nif pkg.__name__ == \"__main__\":\n    pass\n\"\"\",\n        # Compare left is Name but not __name__\n        \"\"\"\nname = \"x\"\nif name == \"__main__\":\n    pass\n\"\"\",\n    ],\n)\ndef test_parse_setup_py_no_setup_call_branches(tmp_path, content):\n    tmp_path.joinpath(\"setup.py\").write_text(textwrap.dedent(content))\n    assert Setup.from_directory(tmp_path) == Setup()\n\n\ndef test_parse_setup_py_irrelevant_call_and_assignment(tmp_path):\n    content = textwrap.dedent(\n        \"\"\"\n        def foo():\n            return 1\n        x = 1\n        foo()\n        # no setup() call here\n        \"\"\"\n    )\n    tmp_path.joinpath(\"setup.py\").write_text(content)\n    assert Setup.from_directory(tmp_path) == Setup()\n\n\n@pytest.mark.parametrize(\n    \"content, expected\",\n    [\n        # No sections -> defaults (version defaults to \"0.0.0\")\n        (\"\", Setup(version=\"0.0.0\")),\n        # Empty metadata section only\n        (\"[metadata]\\n\", Setup(version=\"0.0.0\")),\n        # Empty options section only\n        (\"[options]\\n\", Setup(version=\"0.0.0\")),\n        # Name only, version still defaults to 0.0.0\n        (\"[metadata]\\nname = foo\\n\", Setup(name=\"foo\", version=\"0.0.0\")),\n        # Version attr -> keep default 0.0.0\n        (\"[metadata]\\nversion = attr:foo.__version__\\n\", Setup(version=\"0.0.0\")),\n    ],\n)\ndef test_parse_setup_cfg_missing_sections_and_options(tmp_path, content, expected):\n    tmp_path.joinpath(\"setup.cfg\").write_text(content)\n    assert Setup.from_directory(tmp_path) == expected\n\n\ndef test_find_setup_call_in_function_and_if_main(tmp_path):\n    content = textwrap.dedent(\n        \"\"\"\n        from setuptools import setup\n\n        def inner():\n            setup(name=\"foo\", version=\"0.1.0\")\n\n        if __name__ == \"__main__\":\n            inner()\n        \"\"\"\n    )\n    tmp_path.joinpath(\"setup.py\").write_text(content)\n    assert Setup.from_directory(tmp_path) == Setup(name=\"foo\", version=\"0.1.0\")\n\n\ndef test_if_main_direct_setup_hits_concat_body_return(tmp_path):\n    content = textwrap.dedent(\n        \"\"\"\n        if __name__ == \"__main__\":\n            from setuptools import setup\n            setup(name=\"foo2\", version=\"0.3.0\")\n        \"\"\"\n    )\n    tmp_path.joinpath(\"setup.py\").write_text(content)\n    assert Setup.from_directory(tmp_path) == Setup(name=\"foo2\", version=\"0.3.0\")\n\n\ndef test_from_directory_precedence_and_falsy_update(tmp_path):\n    pyproject = textwrap.dedent(\n        \"\"\"\n        [project]\n        name = \"name_py\"\n        version = \"0.1.0\"\n        requires-python = \">=3.7\"\n        dependencies = [\"a\"]\n        [project.optional-dependencies]\n        tui = [\"r1\"]\n        \"\"\"\n    )\n    tmp_path.joinpath(\"pyproject.toml\").write_text(pyproject)\n\n    setup_cfg = textwrap.dedent(\n        \"\"\"\n        [metadata]\n        name = name_cfg\n        version = 0.1.1\n\n        [options]\n        python_requires = >=3.8\n        install_requires =\n            b\n\n        [options.extras_require]\n        tui =\n            r2\n        \"\"\"\n    )\n    tmp_path.joinpath(\"setup.cfg\").write_text(setup_cfg)\n\n    setup_py = textwrap.dedent(\n        \"\"\"\n        from setuptools import setup\n        setup(name='name_py', version='0.2.0', install_requires=[], extras_require={'tui': ['r3']})\n        \"\"\"\n    )\n    tmp_path.joinpath(\"setup.py\").write_text(setup_py)\n\n    result = Setup.from_directory(tmp_path)\n    assert result.name == \"name_py\"  # from setup.py (last wins)\n    assert result.version == \"0.2.0\"  # from setup.py\n    assert result.install_requires == [\"b\"]  # not overridden by empty list in setup.py\n    assert result.extras_require == {\"tui\": [\"r3\"]}  # overridden by setup.py (truthy)\n    assert result.python_requires == \">=3.8\"  # from setup.cfg (later non-empty vs pyproject)\n\n\ndef test_read_pyproject_toml_project_error_returns_empty(tmp_path, mocker):\n    # Create a dummy PyProject that raises ProjectError on unwrap()\n    import pdm.project.project_file as project_file\n\n    mocker.patch.object(project_file.PyProject, \"_convert_pyproject\", side_effect=ProjectError(\"boom\"))\n\n    tmp_path.joinpath(\"pyproject.toml\").write_text(\"[project]\\nname='x'\")\n    assert Setup.from_directory(tmp_path) == Setup()  # empty due to ProjectError\n\n\ndef test_read_pyproject_toml_metaconverter_error_uses_partial_data_and_logs(tmp_path, mocker, caplog):\n    partial = {\n        \"name\": \"foo\",\n        \"version\": \"0.1.0\",\n        \"description\": \"desc\",\n        \"dependencies\": [\"a\"],\n        \"optional-dependencies\": {\"tui\": [\"r\"]},\n        \"requires-python\": \">=3.8\",\n    }\n\n    import pdm.project.project_file as project_file\n\n    mocker.patch.object(\n        project_file.PyProject, \"_convert_pyproject\", side_effect=MetaConvertError([\"e1\"], data=partial, settings={})\n    )\n\n    caplog.set_level(\"WARNING\")\n    logging.getLogger(\"pdm.termui\").addHandler(caplog.handler)\n    tmp_path.joinpath(\"pyproject.toml\").write_text(\"[tool.other]\\nfoo='bar'\")\n    result = Setup.from_directory(tmp_path)\n\n    # Check fields are populated from partial data\n    assert result.name == \"foo\"\n    assert result.version == \"0.1.0\"\n    assert result.summary == \"desc\"\n    assert result.install_requires == [\"a\"]\n    assert result.extras_require == {\"tui\": [\"r\"]}\n    assert result.python_requires == \">=3.8\"\n\n    # Check a warning was logged\n    assert any(\"Error parsing pyproject.toml\" in message for message in caplog.messages)\n\n\ndef test_setup_distribution_metadata_and_requires_markers():\n    dist = Setup(\n        name=\"pack\",\n        version=\"1.0.0\",\n        summary=\"desc\",\n        python_requires=\">=3.8\",\n        install_requires=[\"base>=1\"],\n        extras_require={\n            \"tui\": [\n                \"rich\",\n                'foo; python_version >= \"3.9\"',\n                'bar; python_version < \"3.8\" or sys_platform == \"win32\"',\n            ]\n        },\n    ).as_dist()\n\n    meta = dist.metadata\n    assert meta == {\n        \"Name\": \"pack\",\n        \"Version\": \"1.0.0\",\n        \"Summary\": \"desc\",\n        \"Requires-Python\": \">=3.8\",\n    }\n\n    reqs = dist.requires\n    # Contains base\n    assert any(r.startswith(\"base>=1\") for r in reqs)\n    # Extra-only requirement with no existing marker\n    assert any(r.startswith(\"rich\") and 'extra == \"tui\"' in r for r in reqs)\n    # Existing marker combined without parentheses\n    assert any(r.startswith(\"foo\") and 'python_version >= \"3.9\" and extra == \"tui\"' in r for r in reqs)\n    # Existing marker with OR and repeated extra markers (no parentheses)\n    assert any(\n        r.startswith(\"bar\")\n        and 'python_version < \"3.8\" and extra == \"tui\" or sys_platform == \"win32\" and extra == \"tui\"' in r\n        for r in reqs\n    )\n\n\ndef test_read_text_and_locate_file_smoke():\n    dist = Setup(name=\"x\", version=\"1\").as_dist()\n    assert dist.read_text(\"any\") is None\n    p = dist.locate_file(\"whatever\")\n    from pathlib import Path as _P\n\n    assert isinstance(p, _P)\n\n\ndef test_find_setup_call_skips_non_call_expr_then_finds_setup(tmp_path):\n    content = textwrap.dedent(\n        \"\"\"\n        \"module docstring\"\n        from setuptools import setup\n        setup(name='d', version='0.1.0')\n        \"\"\"\n    )\n    tmp_path.joinpath(\"setup.py\").write_text(content)\n    assert Setup.from_directory(tmp_path) == Setup(name=\"d\", version=\"0.1.0\")\n\n\ndef test_find_setup_call_skips_non_functiondef_element_then_finds_setup(tmp_path):\n    content = textwrap.dedent(\n        \"\"\"\n        class X: pass\n        from setuptools import setup\n        setup(name='e', version='0.1.0')\n        \"\"\"\n    )\n    tmp_path.joinpath(\"setup.py\").write_text(content)\n    assert Setup.from_directory(tmp_path) == Setup(name=\"e\", version=\"0.1.0\")\n\n\ndef test_find_setup_call_skips_non_setup_call_then_finds_setup(tmp_path):\n    content = textwrap.dedent(\n        \"\"\"\n        def foo(): pass\n        foo()\n        from setuptools import setup\n        setup(name='f', version='0.1.0')\n        \"\"\"\n    )\n    tmp_path.joinpath(\"setup.py\").write_text(content)\n    assert Setup.from_directory(tmp_path) == Setup(name=\"f\", version=\"0.1.0\")\n\n\ndef test_install_requires_name_resolves_to_non_list(tmp_path):\n    content = textwrap.dedent(\n        \"\"\"\n        from setuptools import setup\n        reqs = 'notalist'\n        setup(name='g', version='0.1.0', install_requires=reqs)\n        \"\"\"\n    )\n    tmp_path.joinpath(\"setup.py\").write_text(content)\n    result = Setup.from_directory(tmp_path)\n    assert result.name == \"g\"\n    assert result.install_requires == []\n\n\ndef test_extras_require_name_is_undefined_returns_empty(tmp_path):\n    content = textwrap.dedent(\n        \"\"\"\n        from setuptools import setup\n        setup(name='h', version='0.1.0', extras_require=extras)\n        \"\"\"\n    )\n    tmp_path.joinpath(\"setup.py\").write_text(content)\n    result = Setup.from_directory(tmp_path)\n    assert result.extras_require == {}\n\n\ndef test_extras_require_name_resolves_to_non_dict(tmp_path):\n    content = textwrap.dedent(\n        \"\"\"\n        from setuptools import setup\n        extras = ['x']\n        setup(name='i', version='0.1.0', extras_require=extras)\n        \"\"\"\n    )\n    tmp_path.joinpath(\"setup.py\").write_text(content)\n    result = Setup.from_directory(tmp_path)\n    assert result.extras_require == {}\n\n\ndef test_single_string_name_variable_not_string(tmp_path):\n    content = textwrap.dedent(\n        \"\"\"\n        from setuptools import setup\n        namev = 123\n        setup(name=namev, version='0.1.0')\n        \"\"\"\n    )\n    tmp_path.joinpath(\"setup.py\").write_text(content)\n    result = Setup.from_directory(tmp_path)\n    assert result.name is None\n\n\ndef test_extras_require_from_kwargs_non_dict_non_call_returns_empty(tmp_path):\n    content = textwrap.dedent(\n        \"\"\"\n        from setuptools import setup\n        kwargs = \"notadict\"\n        setup(**kwargs)\n        \"\"\"\n    )\n    tmp_path.joinpath(\"setup.py\").write_text(content)\n    result = Setup.from_directory(tmp_path)\n    assert result.extras_require == {}\n    assert result.version == \"0.0.0\"\n\n\ndef test_extras_require_from_kwargs_call_func_is_attribute_returns_empty(tmp_path):\n    content = textwrap.dedent(\n        \"\"\"\n        from setuptools import setup\n        class Obj:\n            def build(self):\n                return {}\n        obj = Obj()\n        kwargs = obj.build()\n        setup(**kwargs)\n        \"\"\"\n    )\n    tmp_path.joinpath(\"setup.py\").write_text(content)\n    result = Setup.from_directory(tmp_path)\n    assert result.extras_require == {}\n\n\ndef test_extras_require_from_kwargs_call_func_name_not_dict_returns_empty(tmp_path):\n    content = textwrap.dedent(\n        \"\"\"\n        from setuptools import setup\n        def make_kwargs(**kw):\n            return kw\n        kwargs = make_kwargs(name='n', version='0.1.0')\n        setup(**kwargs)\n        \"\"\"\n    )\n    tmp_path.joinpath(\"setup.py\").write_text(content)\n    result = Setup.from_directory(tmp_path)\n    # kwargs created by a function call that is not dict() won't be introspected\n    assert result.name is None\n    assert result.version == \"0.0.0\"\n    assert result.extras_require == {}\n"
  },
  {
    "path": "tests/models/test_specifiers.py",
    "content": "import pytest\n\nfrom pdm.models.specifiers import PySpecSet\n\n\n@pytest.mark.filterwarnings(\"ignore::FutureWarning\")\n@pytest.mark.parametrize(\n    \"original,normalized\",\n    [\n        (\">=3.6\", \">=3.6\"),\n        (\"<3.8\", \"<3.8\"),\n        (\"~=2.7.0\", \"~=2.7.0\"),\n        (\"\", \"\"),\n        (\">=3.6,<3.8\", \"<3.8,>=3.6\"),\n        (\">3.6\", \">3.6\"),\n        (\"<=3.7\", \"<=3.7\"),\n        (\">=3.4.*\", \">=3.4.0\"),\n        (\">3.4.*\", \">=3.4.0\"),\n        (\"<=3.4.*\", \"<3.4.0\"),\n        (\"<3.4.*\", \"<3.4.0\"),\n        (\">=3.0+g1234\", \">=3.0\"),\n        (\"<3.0+g1234\", \"<3.0\"),\n        (\"<3.10.0a6\", \"<3.10.0a6\"),\n        (\"<3.10.2a3\", \"<3.10.2a3\"),\n    ],\n)\ndef test_normalize_pyspec(original, normalized):\n    spec = PySpecSet(original)\n    assert str(spec) == normalized\n\n\n@pytest.mark.parametrize(\n    \"left,right,result\",\n    [\n        (\">=3.6\", \">=3.0\", \">=3.6\"),\n        (\">=3.6\", \"<3.8\", \"<3.8,>=3.6\"),\n        (\"\", \">=3.6\", \">=3.6\"),\n        (\">=3.6\", \"<3.2\", \"<empty>\"),\n        (\">=2.7,!=3.0.*\", \"!=3.1.*\", \"!=3.0.*,!=3.1.*,>=2.7\"),\n        (\">=3.11.0a2\", \"<3.11.0b\", \">=3.11.0a2,<3.11.0b0\"),\n        (\"<3.11.0a2\", \">3.11.0b\", \"<empty>\"),\n    ],\n)\ndef test_pyspec_and_op(left, right, result):\n    left = PySpecSet(left)\n    right = PySpecSet(right)\n    assert left & right == PySpecSet(result)\n\n\n@pytest.mark.parametrize(\n    \"left,right,result\",\n    [\n        (\">=3.6\", \">=3.0\", \">=3.0\"),\n        (\"\", \">=3.6\", \"\"),\n        (\">=3.6\", \"<3.7\", \"\"),\n        (\">=3.6,<3.8\", \">=3.4,<3.7\", \"<3.8,>=3.4\"),\n        (\"~=2.7\", \">=3.6\", \"!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7\"),\n        (\"<2.7.15\", \">=3.0\", \"!=2.7.15,!=2.7.16,!=2.7.17,!=2.7.18\"),\n        (\">3.11.0a2\", \">3.11.0b\", \">3.11.0a2\"),\n    ],\n)\ndef test_pyspec_or_op(left, right, result):\n    left = PySpecSet(left)\n    right = PySpecSet(right)\n    assert str(left | right) == result\n\n\ndef test_impossible_pyspec():\n    spec = PySpecSet(\">=3.6,<3.4\")\n    a = PySpecSet(\">=2.7\")\n    assert spec.is_empty()\n    assert (spec & a).is_empty()\n    assert spec | a == a\n\n\n@pytest.mark.filterwarnings(\"ignore::FutureWarning\")\n@pytest.mark.parametrize(\n    \"left,right\",\n    [\n        (\"~=2.7\", \">=2.7\"),\n        (\">=3.6\", \"\"),\n        (\">=3.7\", \">=3.6,<4.0\"),\n        (\">=2.7,<3.0\", \">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*\"),\n        (\">=3.6\", \">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*\"),\n        (\n            \">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*\",\n            \">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*\",\n        ),\n        (\">=3.11.*\", \">=3.11.0rc\"),\n    ],\n)\ndef test_pyspec_is_subset_superset(left, right):\n    left = PySpecSet(left)\n    right = PySpecSet(right)\n    assert left.is_subset(right), f\"{left}, {right}\"\n    assert right.is_superset(left), f\"{left}, {right}\"\n\n\n@pytest.mark.parametrize(\n    \"left,right\",\n    [\n        (\"~=2.7\", \">=2.6,<2.7.15\"),\n        (\">=3.7\", \">=3.6,<3.9\"),\n        (\">=3.7,<3.6\", \"==2.7\"),\n        (\">=3.0,!=3.4.*\", \">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*\"),\n        (\">=3.11.0\", \"<3.11.0a\"),\n    ],\n)\ndef test_pyspec_isnot_subset_superset(left, right):\n    left = PySpecSet(left)\n    right = PySpecSet(right)\n    assert not left.is_subset(right), f\"{left}, {right}\"\n    assert not left.is_superset(right), f\"{left}, {right}\"\n"
  },
  {
    "path": "tests/models/test_versions.py",
    "content": "import pytest\n\nfrom pdm.models.versions import InvalidPyVersion, Version\n\n\ndef test_unsupported_post_version() -> None:\n    with pytest.raises(InvalidPyVersion):\n        Version(\"3.10.0post1\")\n\n\ndef test_support_prerelease_version() -> None:\n    assert not Version(\"3.9.0\").is_prerelease\n    v = Version(\"3.9.0a4\")\n    assert v.is_prerelease\n    assert str(v) == \"3.9.0a4\"\n    assert v.complete() == v\n    assert v.bump() == Version(\"3.9.0a5\")\n    assert v.bump(2) == Version(\"3.9.1\")\n\n\ndef test_normalize_non_standard_version():\n    version = Version(\"3.9*\")\n    assert str(version) == \"3.9.*\"\n\n\ndef test_version_comparison():\n    assert Version(\"3.9.0\") < Version(\"3.9.1\")\n    assert Version(\"3.4\") < Version(\"3.9.1\")\n    assert Version(\"3.7.*\") < Version(\"3.7.5\")\n    assert Version(\"3.7\") == Version((3, 7))\n\n    assert Version(\"3.9.0a\") != Version(\"3.9.0\")\n    assert Version(\"3.9.0a\") == Version(\"3.9.0a0\")\n    assert Version(\"3.10.0a9\") < Version(\"3.10.0a12\")\n    assert Version(\"3.10.0a12\") < Version(\"3.10.0b1\")\n    assert Version(\"3.7.*\") < Version(\"3.7.1b\")\n\n\ndef test_version_is_wildcard():\n    assert not Version(\"3\").is_wildcard\n    assert Version(\"3.*\").is_wildcard\n\n\ndef test_version_is_py2():\n    assert not Version(\"3.8\").is_py2\n    assert Version(\"2.7\").is_py2\n\n\n@pytest.mark.parametrize(\n    \"version,args,result\",\n    [(\"3.9\", (), \"3.9.0\"), (\"3.9\", (\"*\",), \"3.9.*\"), (\"3\", (0, 2), \"3.0\")],\n)\ndef test_version_complete(version, args, result):\n    assert str(Version(version).complete(*args)) == result\n\n\n@pytest.mark.parametrize(\n    \"version,idx,result\",\n    [\n        (\"3.8.0\", -1, \"3.8.1\"),\n        (\"3.8\", -1, \"3.9.0\"),\n        (\"3\", 0, \"4.0.0\"),\n        (\"3.8.1\", 1, \"3.9.0\"),\n    ],\n)\ndef test_version_bump(version, idx, result):\n    assert str(Version(version).bump(idx)) == result\n\n\n@pytest.mark.parametrize(\n    \"version,other,result\",\n    [\n        (\"3.8.0\", \"3.8\", True),\n        (\"3.8.*\", \"3.8\", True),\n        (\"3.8.1\", \"3.7\", False),\n        (\"3.8\", \"3.8.2\", False),\n    ],\n)\ndef test_version_startswith(version, other, result):\n    assert Version(version).startswith(Version(other)) is result\n\n\ndef test_version_getitem():\n    version = Version(\"3.8.6\")\n    assert version[0] == 3\n    assert version[1] == 8\n    assert version[2] == 6\n    assert version[1:2] == Version(\"8\")\n    assert version[:-1] == Version(\"3.8\")\n\n\ndef test_version_setitem():\n    version = Version(\"3.8.*\")\n    version1 = version.complete()\n    version1[-1] = 0\n    assert version1 == Version(\"3.8.0\")\n\n    version2 = version.complete()\n    version2[0] = 4\n    assert version2 == Version(\"4.8.*\")\n\n    version3 = version.complete()\n    with pytest.raises(TypeError):\n        version3[:2] = (1, 2)\n"
  },
  {
    "path": "tests/resolver/__init__.py",
    "content": ""
  },
  {
    "path": "tests/resolver/test_graph.py",
    "content": "import itertools\n\nfrom pdm.resolver.graph import OrderedSet\n\n\ndef test_ordered_set():\n    elems = [\"A\", \"bb\", \"c3\"]\n    all_sets = set()\n    for case in itertools.permutations(elems):\n        s = OrderedSet(case)\n        all_sets.add(s)\n        assert list(s) == list(case)\n        assert len(s) == len(case)\n        for e in elems:\n            assert e in s\n            assert e + \"1\" not in s\n        assert str(s) == f\"{{{', '.join(map(repr, case))}}}\"\n        assert repr(s) == f\"OrderedSet({{{', '.join(map(repr, case))}}})\"\n\n    assert len(all_sets) == 1\n"
  },
  {
    "path": "tests/resolver/test_resolve.py",
    "content": "import pytest\nfrom resolvelib.resolvers import ResolutionImpossible\n\nfrom pdm.cli.actions import resolve_candidates_from_lockfile\nfrom pdm.exceptions import PackageWarning\nfrom pdm.models.markers import EnvSpec\nfrom pdm.models.requirements import parse_requirement\nfrom pdm.models.specifiers import PySpecSet\nfrom pdm.project.lockfile import FLAG_DIRECT_MINIMAL_VERSIONS, FLAG_INHERIT_METADATA\nfrom pdm.resolver.reporters import LockReporter\nfrom pdm.utils import get_requirement_from_override\nfrom tests import FIXTURES\n\n\n@pytest.fixture()\ndef resolve(project, repository):\n    def resolve_func(\n        lines,\n        requires_python=None,\n        allow_prereleases=None,\n        strategy=\"all\",\n        tracked_names=None,\n        direct_minimal_versions=False,\n        inherit_metadata=False,\n        platform=None,\n    ):\n        env_spec = project.environment.allow_all_spec\n        replace_dict = {}\n        if requires_python:\n            replace_dict[\"requires_python\"] = PySpecSet(requires_python)\n        if platform:\n            replace_dict[\"platform\"] = platform\n        env_spec = env_spec.replace(**replace_dict)\n        if allow_prereleases is not None:\n            project.pyproject.settings.setdefault(\"resolution\", {})[\"allow-prereleases\"] = allow_prereleases\n        requirements = []\n        for line in lines:\n            if line.startswith(\"-e \"):\n                requirements.append(parse_requirement(line[3:], True))\n            else:\n                requirements.append(parse_requirement(line))\n\n        ui = project.core.ui\n        strategies = project.lockfile.default_strategies.copy()\n        if inherit_metadata:\n            strategies.add(FLAG_INHERIT_METADATA)\n        if direct_minimal_versions:\n            strategies.add(FLAG_DIRECT_MINIMAL_VERSIONS)\n\n        with ui.logging(\"lock\"):\n            resolver = project.get_resolver()(\n                environment=project.environment,\n                requirements=requirements,\n                update_strategy=strategy,\n                strategies=strategies,\n                target=env_spec,\n                tracked_names=tracked_names,\n                reporter=LockReporter(),\n            )\n            return resolver.resolve().candidates\n\n    return resolve_func\n\n\ndef test_resolve_named_requirement(resolve):\n    result = resolve([\"requests\"])\n\n    assert result[\"requests\"].version == \"2.19.1\"\n    assert result[\"urllib3\"].version == \"1.22\"\n    assert result[\"chardet\"].version == \"3.0.4\"\n    assert result[\"certifi\"].version == \"2018.11.17\"\n    assert result[\"idna\"].version == \"2.7\"\n\n\ndef test_resolve_exclude(resolve, project):\n    project.pyproject.settings.setdefault(\"resolution\", {})[\"excludes\"] = [\"urllib3\"]\n    result = resolve([\"requests\"])\n\n    assert result[\"requests\"].version == \"2.19.1\"\n    assert result[\"chardet\"].version == \"3.0.4\"\n    assert result[\"certifi\"].version == \"2018.11.17\"\n    assert result[\"idna\"].version == \"2.7\"\n    assert \"urllib3\" not in result\n\n\ndef test_resolve_requires_python(resolve, project):\n    project.environment.python_requires = PySpecSet(\">=2.7\")\n    with pytest.warns(PackageWarning) as records:\n        result = resolve([\"django\"])\n    assert len(records) > 0\n    assert result[\"django\"].version == \"1.11.8\"\n    assert \"sqlparse\" not in result\n\n    result = resolve([\"django\"], \">=3.6\")\n    assert result[\"django\"].version == \"2.2.9\"\n    assert \"sqlparse\" in result\n\n    result = resolve([\"django; python_version>='3.7'\"])\n    assert result[\"django\"].version == \"2.2.9\"\n    assert \"sqlparse\" in result\n\n\ndef test_resolve_allow_prereleases(resolve, repository):\n    repository.add_candidate(\"foo\", \"1.0.0\")\n    repository.add_candidate(\"foo\", \"1.1.0-alpha\")\n    repository.add_candidate(\"bar\", \"1.0.0-beta\")\n\n    result = resolve([\"foo\"])\n    assert result[\"foo\"].version == \"1.0.0\"\n\n    result = resolve([\"foo\"], allow_prereleases=True)\n    assert result[\"foo\"].version == \"1.1.0-alpha\"\n\n    result = resolve([\"foo==1.1.0a0\"])\n    assert result[\"foo\"].version == \"1.1.0-alpha\"\n\n    result = resolve([\"bar\"])\n    assert result[\"bar\"].version == \"1.0.0-beta\"\n\n    with pytest.raises(ResolutionImpossible):\n        resolve([\"bar\"], allow_prereleases=False)\n\n\ndef test_resolve_prereleases_if_disabled_by_project(resolve):\n    result = resolve([\"urllib3==1.23b0\"], allow_prereleases=False)\n    assert result[\"urllib3\"].version == \"1.23b0\"\n\n\ndef test_resolve_with_extras(resolve):\n    result = resolve([\"requests[socks]\"])\n    assert result[\"pysocks\"].version == \"1.5.6\"\n    assert result[\"urllib3\"].version == \"1.22\"\n    assert result[\"chardet\"].version == \"3.0.4\"\n    assert result[\"certifi\"].version == \"2018.11.17\"\n    assert result[\"idna\"].version == \"2.7\"\n    assert result[\"requests\"].version == \"2.19.1\"\n\n\ndef test_resolve_with_extras_and_excludes(resolve, project):\n    project.pyproject.settings.setdefault(\"resolution\", {})[\"excludes\"] = [\"requests\"]\n    result = resolve([\"requests[socks]\"])\n    assert result[\"pysocks\"].version == \"1.5.6\"\n    assert \"requests\" not in result\n    assert \"urllib3\" not in result\n\n\n@pytest.mark.parametrize(\n    \"requirement_line\",\n    [\n        f\"{(FIXTURES / 'artifacts/demo-0.0.1.tar.gz').as_posix()}\",\n        f\"{(FIXTURES / 'artifacts/demo-0.0.1-py2.py3-none-any.whl').as_posix()}\",\n    ],\n    ids=[\"sdist\", \"wheel\"],\n)\ndef test_resolve_local_artifacts(resolve, requirement_line):\n    result = resolve([requirement_line], \">=3.6\")\n    assert result[\"idna\"].version == \"2.7\"\n\n\n@pytest.mark.parametrize(\n    \"line\",\n    [\n        (FIXTURES / \"projects/demo\").as_posix(),\n        \"git+https://github.com/test-root/demo.git#egg=demo\",\n    ],\n)\ndef test_resolve_vcs_and_local_requirements(resolve, line, is_editable, vcs):\n    editable = \"-e \" if is_editable else \"\"\n    result = resolve([editable + line], \">=3.6\")\n    assert result[\"idna\"].version == \"2.7\"\n\n\ndef test_resolve_vcs_without_explicit_name(resolve, vcs):\n    requirement = \"git+https://github.com/test-root/demo.git\"\n    result = resolve([requirement], \">=3.6\")\n    assert result[\"idna\"].version == \"2.7\"\n\n\ndef test_resolve_local_and_named_requirement(resolve, vcs):\n    requirements = [\"demo\", \"git+https://github.com/test-root/demo.git#egg=demo\"]\n    result = resolve(requirements, \">=3.6\")\n    assert result[\"demo\"].req.is_vcs\n\n    requirements = [\"git+https://github.com/test-root/demo.git#egg=demo\", \"demo\"]\n    result = resolve(requirements, \">=3.6\")\n    assert result[\"demo\"].req.is_vcs\n\n\ndef test_resolving_auto_avoid_conflicts(resolve, repository):\n    repository.add_candidate(\"foo\", \"0.1.0\")\n    repository.add_candidate(\"foo\", \"0.2.0\")\n    repository.add_dependencies(\"foo\", \"0.1.0\", [\"hoho<2.0\"])\n    repository.add_dependencies(\"foo\", \"0.2.0\", [\"hoho>=2.0\"])\n    repository.add_candidate(\"bar\", \"0.1.0\")\n    repository.add_dependencies(\"bar\", \"0.1.0\", [\"hoho~=1.1\"])\n    repository.add_candidate(\"hoho\", \"2.1\")\n    repository.add_candidate(\"hoho\", \"1.5\")\n\n    result = resolve([\"foo\", \"bar\"])\n    assert result[\"foo\"].version == \"0.1.0\"\n    assert result[\"bar\"].version == \"0.1.0\"\n    assert result[\"hoho\"].version == \"1.5\"\n\n\ndef test_resolve_conflicting_dependencies(resolve, repository):\n    repository.add_candidate(\"foo\", \"0.1.0\")\n    repository.add_dependencies(\"foo\", \"0.1.0\", [\"hoho>=2.0\"])\n    repository.add_candidate(\"bar\", \"0.1.0\")\n    repository.add_dependencies(\"bar\", \"0.1.0\", [\"hoho~=1.1\"])\n    repository.add_candidate(\"hoho\", \"2.1\")\n    repository.add_candidate(\"hoho\", \"1.5\")\n    with pytest.raises(ResolutionImpossible):\n        resolve([\"foo\", \"bar\"])\n\n\n@pytest.mark.parametrize(\"overrides\", [\"2.1\", \">=1.8\", \"==2.1\"])\ndef test_resolve_conflicting_dependencies_with_overrides(project, resolve, repository, overrides):\n    repository.add_candidate(\"foo\", \"0.1.0\")\n    repository.add_dependencies(\"foo\", \"0.1.0\", [\"hoho>=2.0\"])\n    repository.add_candidate(\"bar\", \"0.1.0\")\n    repository.add_dependencies(\"bar\", \"0.1.0\", [\"hoho~=1.1\"])\n    repository.add_candidate(\"baz\", \"0.1.0\")\n    repository.add_dependencies(\"baz\", \"0.1.0\", [\"hoho[extra]~=1.1\"])\n    repository.add_candidate(\"hoho\", \"2.1\")\n    repository.add_candidate(\"hoho\", \"1.5\")\n    project.pyproject.settings[\"resolution\"] = {\"overrides\": {\"hoho\": overrides}}\n    result = resolve([\"foo\", \"bar\"])\n    assert result[\"hoho\"].version == \"2.1\"\n    result = resolve([\"foo\", \"baz\"])\n    assert result[\"hoho\"].version == \"2.1\"\n\n\ndef test_resolve_no_available_versions(resolve, repository):\n    repository.add_candidate(\"foo\", \"0.1.0\")\n    with pytest.raises(ResolutionImpossible):\n        resolve([\"foo>=0.2.0\"])\n\n\ndef test_exclude_incompatible_requirements(resolve, repository):\n    repository.add_candidate(\"foo\", \"0.1.0\")\n    repository.add_dependencies(\"foo\", \"0.1.0\", [\"bar; python_version < '3'\"])\n    result = resolve([\"foo\"], \">=3.6\")\n    assert \"bar\" not in result\n\n\ndef test_union_markers_from_different_parents(resolve, repository):\n    repository.add_candidate(\"foo\", \"0.1.0\")\n    repository.add_dependencies(\"foo\", \"0.1.0\", [\"bar; python_version < '3'\"])\n    repository.add_candidate(\"bar\", \"0.1.0\")\n    result = resolve([\"foo\", \"bar\"], \">=3.6\")\n    assert not result[\"bar\"].requires_python\n\n\ndef test_requirements_from_different_groups(resolve, repository):\n    repository.add_candidate(\"foo\", \"0.1.0\")\n    repository.add_candidate(\"foo\", \"0.2.0\")\n    requirements = [\"foo\", \"foo<0.2.0\"]\n    result = resolve(requirements)\n    assert result[\"foo\"].version == \"0.1.0\"\n\n\ndef test_resolve_two_extras_from_the_same_package(resolve):\n    # Case borrowed from pypa/pip#7096\n    line = (FIXTURES / \"projects/demo_extras\").as_posix() + \"[extra1,extra2]\"\n    result = resolve([line])\n    assert \"pysocks\" in result\n    assert \"pyopenssl\" in result\n\n\ndef test_resolve_package_with_dummy_upbound(resolve, repository):\n    repository.add_candidate(\"foo\", \"0.1.0\", \">=3.5,<4.0\")\n    result = resolve([\"foo\"], \">=3.5\")\n    assert \"foo\" in result\n\n\ndef test_resolve_dependency_with_extra_marker(resolve, repository):\n    repository.add_candidate(\"foo\", \"0.1.0\")\n    repository.add_dependencies(\"foo\", \"0.1.0\", [\"pytz; extra=='tz' or extra=='all'\"])\n    result = resolve([\"foo\"])\n    assert \"pytz\" not in result\n\n    result = resolve([\"foo[tz]\"])\n    assert \"pytz\" in result\n\n\ndef test_resolve_circular_dependencies(resolve, repository):\n    repository.add_candidate(\"foo\", \"0.1.0\")\n    repository.add_dependencies(\"foo\", \"0.1.0\", [\"foobar\"])\n    repository.add_candidate(\"foobar\", \"0.2.0\")\n    repository.add_dependencies(\"foobar\", \"0.2.0\", [\"foo\"])\n    result = resolve([\"foo\"])\n    assert result[\"foo\"].version == \"0.1.0\"\n    assert result[\"foobar\"].version == \"0.2.0\"\n\n\ndef test_resolve_candidates_to_install(project):\n    project.lockfile.set_data(\n        {\n            \"metadata\": {\"strategy\": [\"cross_platform\"]},\n            \"package\": [\n                {\n                    \"name\": \"pytest\",\n                    \"version\": \"4.6.0\",\n                    \"summary\": \"pytest module\",\n                    \"dependencies\": [\"py>=3.0\", \"configparser; sys_platform=='win32'\"],\n                },\n                {\n                    \"name\": \"configparser\",\n                    \"version\": \"1.2.0\",\n                    \"summary\": \"configparser module\",\n                    \"dependencies\": [\"backports\"],\n                },\n                {\n                    \"name\": \"py\",\n                    \"version\": \"3.6.0\",\n                    \"summary\": \"py module\",\n                },\n                {\n                    \"name\": \"backports\",\n                    \"version\": \"2.2.0\",\n                    \"summary\": \"backports module\",\n                },\n            ],\n        }\n    )\n    reqs = [parse_requirement(\"pytest\")]\n    result = resolve_candidates_from_lockfile(project, reqs, env_spec=EnvSpec.from_spec(\"==3.11\", \"linux\", \"cpython\"))\n    assert result[\"pytest\"].version == \"4.6.0\"\n    assert result[\"py\"].version == \"3.6.0\"\n    assert \"configparser\" not in result\n    assert \"backports\" not in result\n\n    result = resolve_candidates_from_lockfile(project, reqs, env_spec=EnvSpec.from_spec(\"==3.11\", \"windows\", \"cpython\"))\n    assert result[\"pytest\"].version == \"4.6.0\"\n    assert result[\"py\"].version == \"3.6.0\"\n    assert result[\"configparser\"].version == \"1.2.0\"\n    assert result[\"backports\"].version == \"2.2.0\"\n\n\ndef test_resolve_prefer_requirement_with_prereleases(resolve):\n    result = resolve([\"urllib3\", \"requests>=2.20.0b0\"])\n    assert result[\"urllib3\"].version == \"1.23b0\"\n\n\ndef test_resolve_with_python_marker(resolve):\n    result = resolve([\"demo; python_version>='3.6'\"])\n    assert result[\"demo\"].version == \"0.0.1\"\n\n\ndef test_resolve_file_req_with_prerelease(resolve, vcs):\n    result = resolve(\n        [\n            \"using-demo==0.1.0\",\n            \"demo @ git+https://github.com/test-root/demo-prerelease.git\",\n        ],\n        \">=3.6\",\n        allow_prereleases=False,\n    )\n    assert result[\"demo\"].version == \"0.0.2b0\"\n\n\ndef test_resolve_extra_requirements_no_break_constraints(resolve, repository):\n    repository.add_candidate(\"foo\", \"0.1.0\")\n    repository.add_dependencies(\"foo\", \"0.1.0\", [\"chardet; extra=='chardet'\"])\n    repository.add_candidate(\"foo\", \"0.2.0\")\n    repository.add_dependencies(\"foo\", \"0.2.0\", [\"chardet; extra=='chardet'\"])\n    result = resolve([\"foo[chardet]<0.2.0\"])\n    assert \"chardet\" in result\n    assert result[\"foo\"].version == \"0.1.0\"\n\n\ndef test_resolve_extra_and_underlying_to_the_same_version(resolve, repository):\n    repository.add_candidate(\"foo\", \"0.1.0\")\n    repository.add_dependencies(\"foo\", \"0.1.0\", [\"chardet; extra=='enc'\"])\n    repository.add_candidate(\"foo\", \"0.2.0\")\n    repository.add_dependencies(\"foo\", \"0.2.0\", [\"chardet; extra=='enc'\"])\n    repository.add_candidate(\"bar\", \"0.1.0\")\n    repository.add_dependencies(\"bar\", \"0.1.0\", [\"foo[enc]>=0.1.0\"])\n    result = resolve([\"foo==0.1.0\", \"bar\"])\n    assert result[\"foo\"].version == result[\"foo[enc]\"].version == \"0.1.0\"\n\n\ndef test_resolve_skip_candidate_with_invalid_metadata(resolve, repository):\n    repository.add_candidate(\"sqlparse\", \"0.4.0\")\n    repository.add_dependencies(\"sqlparse\", \"0.4.0\", [\"django>=1.11'\"])\n    result = resolve([\"sqlparse\"], \">=3.6\")\n    assert result[\"sqlparse\"].version == \"0.3.0\"\n\n\ndef test_resolve_direct_minimal_versions(resolve, repository, project):\n    repository.add_candidate(\"pytz\", \"2019.6\")\n    project.add_dependencies([\"django\"])\n    result = resolve([\"django\"], \">=3.6\", direct_minimal_versions=True)\n    assert result[\"django\"].version == \"1.11.8\"\n    assert result[\"pytz\"].version == \"2019.6\"\n\n\ndef test_resolve_record_markers(resolve, repository, project):\n    repository.add_candidate(\"A\", \"1.0\")\n    repository.add_candidate(\"B\", \"1.0\")\n    repository.add_candidate(\"C\", \"1.0\")\n    repository.add_candidate(\"D\", \"1.0\")\n    repository.add_candidate(\"E\", \"1.0\")\n    repository.add_candidate(\"F\", \"1.0\")\n    repository.add_dependencies(\"A\", \"1.0\", [\"B; os_name == 'posix'\", \"C; os_name=='nt'\"])\n    # package D has transitive markers that conflict\n    repository.add_dependencies(\"C\", \"1.0\", [\"D; os_name!='nt'\", \"E; python_version < '3.8'\"])\n    # package E has union markers\n    repository.add_dependencies(\"B\", \"1.0\", [\"E; python_version >= '3.7'\"])\n    # B -> E -> F -> B has circular dependency\n    repository.add_dependencies(\"E\", \"1.0\", [\"F; platform_machine=='x86_64'\"])\n    repository.add_dependencies(\"F\", \"1.0\", [\"B\"])\n\n    result = resolve([\"A\"], \">=3.6\", inherit_metadata=True)\n    assert result[\"a\"].version == \"1.0\"\n    assert \"d\" not in result\n    assert (\n        str(result[\"e\"].req.marker)\n        == 'python_version >= \"3.7\" and os_name == \"posix\" or python_version < \"3.8\" and os_name == \"nt\"'\n    )\n    assert (\n        str(result[\"f\"].req.marker)\n        == 'python_version >= \"3.7\" and os_name == \"posix\" and platform_machine == \"x86_64\" or '\n        'python_version < \"3.8\" and os_name == \"nt\" and platform_machine == \"x86_64\"'\n    )\n    assert (\n        str(result[\"b\"].req.marker) == 'os_name == \"posix\" or (os_name == \"posix\" or os_name == \"nt\") and '\n        'platform_machine == \"x86_64\" and python_version < \"3.8\"'\n    )\n\n\n@pytest.mark.parametrize(\n    \"name,value,output\",\n    [\n        (\"foo\", \"1.0\", \"foo==1.0\"),\n        (\"foo\", \"==1.0\", \"foo==1.0\"),\n        (\"foo\", \">=1.0\", \"foo>=1.0\"),\n        (\"foo\", \"http://foobar.com\", \"foo @ http://foobar.com\"),\n    ],\n)\ndef test_requirement_from_override(name, value, output):\n    assert get_requirement_from_override(name, value) == output\n"
  },
  {
    "path": "tests/resolver/test_uv_resolver.py",
    "content": "from textwrap import dedent\n\nimport pytest\n\nfrom pdm.models.markers import EnvSpec\nfrom pdm.models.requirements import parse_requirement\n\npytestmark = [pytest.mark.network, pytest.mark.uv]\n\n\ndef resolve(environment, requirements, target=None):\n    from pdm.resolver.uv import UvResolver\n\n    reqs = []\n    for req in requirements:\n        if isinstance(req, str):\n            req = parse_requirement(req)\n            req.groups = [\"default\"]\n        reqs.append(req)\n\n    resolver = UvResolver(\n        environment,\n        requirements=reqs,\n        target=target or environment.spec,\n        update_strategy=\"all\",\n        strategies=set(),\n    )\n    return resolver.resolve()\n\n\ndef test_resolve_requirements(project):\n    requirements = [\"requests==2.32.0\", \"urllib3<2\"]\n    resolution = resolve(project.environment, requirements)\n    mapping = {p.candidate.identify(): p.candidate for p in resolution.packages}\n    assert mapping[\"requests\"].version == \"2.32.0\"\n    assert mapping[\"urllib3\"].version.startswith(\"1.26\")\n\n\ndef test_resolve_vcs_requirement(project):\n    requirements = [\"git+https://github.com/pallets/click.git@8.1.0\"]\n    resolution = resolve(project.environment, requirements)\n    mapping = {p.candidate.identify(): p.candidate for p in resolution.packages}\n    assert \"colorama\" in mapping\n    assert mapping[\"click\"].req.is_vcs\n\n\ndef test_resolve_with_python_requires(project):\n    requirements = [\"urllib3<2; python_version<'3.10'\", \"urllib3>=2; python_version>='3.10'\"]\n    if project.python.version_tuple >= (3, 10):\n        resolution = resolve(project.environment, requirements, EnvSpec.from_spec(\">=3.10\"))\n        packages = list(resolution.packages)\n        assert len(packages) == 1\n        assert packages[0].candidate.version.startswith(\"2.\")\n\n    resolution = resolve(project.environment, requirements, EnvSpec.from_spec(\">=3.8\"))\n    packages = list(resolution.packages)\n    assert len(packages) == 2\n\n\ndef test_resolve_dependencies_with_nested_extras(project):\n    name = project.name\n    project.add_dependencies([\"urllib3\"], \"default\", write=False)\n    project.add_dependencies([\"idna\"], \"extra1\", write=False)\n    project.add_dependencies([\"chardet\", f\"{name}[extra1]\"], \"extra2\", write=False)\n    project.add_dependencies([f\"{name}[extra1,extra2]\"], \"all\")\n\n    dependencies = [*project.get_dependencies(), *project.get_dependencies(\"all\")]\n    assert len(dependencies) == 3, [dep.identify() for dep in dependencies]\n    resolution = resolve(project.environment, dependencies)\n    assert resolution.collected_groups == {\"default\", \"extra1\", \"extra2\", \"all\"}\n    mapping = {p.candidate.identify(): p.candidate for p in resolution.packages}\n    assert set(mapping) == {\"urllib3\", \"idna\", \"chardet\"}\n\n\n@pytest.mark.parametrize(\"overrides\", (\"2.31.0\", \"==2.31.0\"))\ndef test_resolve_dependencies_with_overrides(project, overrides):\n    requirements = [\"requests==2.32.0\"]\n\n    project.pyproject.settings[\"resolution\"] = {\"overrides\": {\"requests\": overrides}}\n\n    resolution = resolve(project.environment, requirements)\n\n    mapping = {p.candidate.identify(): p.candidate for p in resolution.packages}\n    assert mapping[\"requests\"].version == \"2.31.0\"\n\n\ndef test_parse_uv_lock_with_source_url_fallback(project):\n    from pdm.resolver.uv import UvResolver\n\n    lock_path = project.root / \"uv.lock\"\n    lock_path.write_text(\n        dedent(\n            \"\"\"\n            version = 1\n            requires-python = \">=3.8\"\n\n            [[package]]\n            name = \"mdformat-py-edu-fr\"\n            version = \"0.1.1\"\n            source = { url = \"http://foss.heptapod.net/py-edu-fr/mdformat-py-edu-fr/-/archive/0.1.1/mdformat-py-edu-fr.tar.gz\" }\n            sdist = { hash = \"sha256:124488d1796a7ad5f98b1365fe00ff3e71846fd1f91d46e54f8b73c0cdbd78a1\" }\n            \"\"\"\n        ).strip(),\n        encoding=\"utf-8\",\n    )\n\n    resolver = UvResolver(\n        project.environment,\n        requirements=[],\n        target=project.environment.spec,\n        update_strategy=\"all\",\n        strategies=set(),\n    )\n    resolution = resolver._parse_uv_lock(lock_path)\n    candidate = next(iter(resolution.packages)).candidate\n\n    assert candidate.req.url == (\n        \"http://foss.heptapod.net/py-edu-fr/mdformat-py-edu-fr/-/archive/0.1.1/mdformat-py-edu-fr.tar.gz\"\n    )\n    assert candidate.hashes[0] == {\n        \"url\": \"http://foss.heptapod.net/py-edu-fr/mdformat-py-edu-fr/-/archive/0.1.1/mdformat-py-edu-fr.tar.gz\",\n        \"file\": \"mdformat-py-edu-fr.tar.gz\",\n        \"hash\": \"sha256:124488d1796a7ad5f98b1365fe00ff3e71846fd1f91d46e54f8b73c0cdbd78a1\",\n    }\n"
  },
  {
    "path": "tests/test_formats.py",
    "content": "import shutil\nfrom argparse import Namespace\n\nimport pytest\n\nfrom pdm.formats import MetaConvertError, flit, pipfile, poetry, requirements, setup_py\nfrom pdm.models.requirements import parse_requirement\nfrom pdm.utils import cd\nfrom tests import FIXTURES\n\n\ndef ns(**kwargs):\n    default_options = {\n        \"dev\": False,\n        \"group\": None,\n        \"expandvars\": False,\n        \"self\": False,\n        \"editable_self\": False,\n        \"hashes\": True,\n    }\n    kwargs = {**default_options, **kwargs}\n    self = kwargs.pop(\"self\")\n    rv = Namespace(**kwargs)\n    rv.self = self\n    return rv\n\n\ndef test_convert_pipfile(project):\n    golden_file = FIXTURES / \"Pipfile\"\n    assert pipfile.check_fingerprint(project, golden_file)\n    result, settings = pipfile.convert(project, golden_file, None)\n\n    assert settings[\"resolution\"][\"allow-prereleases\"]\n    assert result[\"requires-python\"] == \">=3.6\"\n\n    assert not settings.get(\"dev-dependencies\", {}).get(\"dev\")\n\n    assert \"requests\" in result[\"dependencies\"]\n    assert 'pywinusb; sys_platform == \"win32\"' in result[\"dependencies\"]\n\n    assert settings[\"source\"][0][\"url\"] == \"https://pypi.python.org/simple\"\n\n\n@pytest.mark.parametrize(\"is_dev\", [True, False])\ndef test_convert_requirements_file(project, is_dev):\n    golden_file = FIXTURES / \"requirements.txt\"\n    assert requirements.check_fingerprint(project, golden_file)\n    options = ns(dev=is_dev)\n    result, settings = requirements.convert(project, golden_file, options)\n    group = settings[\"dev-dependencies\"][\"dev\"] if is_dev else result[\"dependencies\"]\n    dev_group = settings[\"dev-dependencies\"][\"dev\"]\n\n    assert len(settings[\"source\"]) == 2\n    assert \"webassets==2.0\" in group\n    assert 'whoosh==2.7.4; sys_platform == \"win32\"' in group\n    assert \"-e git+https://github.com/pypa/pip.git@main#egg=pip\" in dev_group\n    if not is_dev:\n        assert \"-e git+https://github.com/pypa/pip.git@main#egg=pip\" not in group\n    assert (\n        \"pep508-package @ git+https://github.com/techalchemy/test-project.git\"\n        \"@master#subdirectory=parent_folder/pep508-package\" in group\n    )\n\n\ndef test_convert_requirements_file_without_name(project, vcs):\n    req_file = project.root.joinpath(\"reqs.txt\")\n    project.root.joinpath(\"reqs.txt\").write_text(\"git+https://github.com/test-root/demo.git\\n\")\n    assert requirements.check_fingerprint(project, str(req_file))\n    result, _ = requirements.convert(project, str(req_file), ns())\n\n    assert result[\"dependencies\"] == [\"demo @ git+https://github.com/test-root/demo.git\"]\n\n\ndef test_convert_poetry(project):\n    golden_file = FIXTURES / \"pyproject.toml\"\n    assert poetry.check_fingerprint(project, golden_file)\n    with cd(FIXTURES):\n        result, settings = poetry.convert(project, golden_file, ns())\n\n    assert result[\"authors\"] == [\n        {\n            \"name\": \"Sébastien Eustace\",\n            \"email\": \"sebastien@eustace.io\",\n        },\n        {\n            \"name\": \"Example, Inc.\",\n            \"email\": \"inc@example.com\",\n        },\n    ]\n    assert result[\"name\"] == \"poetry\"\n    assert result[\"version\"] == \"1.0.0\"\n    assert result[\"license\"] == {\"text\": \"MIT\"}\n    assert \"repository\" in result[\"urls\"]\n    assert result[\"requires-python\"] == \"!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,<4.0,>=2.7\"\n    assert 'cleo<1.0.0,>=0.7.6; python_version ~= \"2.7\"' in result[\"dependencies\"]\n    assert 'cachecontrol[filecache]<1.0.0,>=0.12.4; python_version ~= \"3.4\"' in result[\"dependencies\"]\n    assert \"babel==2.9.0\" in result[\"dependencies\"]\n    assert \"mysql\" in result[\"optional-dependencies\"]\n    assert \"psycopg2<3.0,>=2.7\" in result[\"optional-dependencies\"][\"pgsql\"]\n    assert len(settings[\"dev-dependencies\"][\"dev\"]) == 2\n\n    assert result[\"scripts\"] == {\"poetry\": \"poetry.console:run\"}\n    assert result[\"entry-points\"][\"blogtool.parsers\"] == {\".rst\": \"some_module:SomeClass\"}\n    build = settings[\"build\"]\n    assert build[\"includes\"] == [\"lib/my_package\", \"tests\", \"CHANGELOG.md\"]\n    assert build[\"excludes\"] == [\"my_package/excluded.py\"]\n\n\ndef test_convert_poetry_12(project):\n    golden_file = FIXTURES / \"poetry-new.toml\"\n    with cd(FIXTURES):\n        result, settings = poetry.convert(project, golden_file, ns())\n\n    assert result[\"dependencies\"] == [\"httpx\", \"pendulum\"]\n    assert settings[\"dev-dependencies\"][\"test\"] == [\"pytest<7.0.0,>=6.0.0\", \"pytest-mock\"]\n\n\ndef test_convert_flit(project):\n    golden_file = FIXTURES / \"projects/flit-demo/pyproject.toml\"\n    assert flit.check_fingerprint(project, golden_file)\n    result, settings = flit.convert(project, golden_file, None)\n\n    assert result[\"name\"] == \"pyflit\"\n    assert result[\"version\"] == \"0.1.0\"\n    assert result[\"description\"] == \"An awesome flit demo\"\n    assert \"classifiers\" in result[\"dynamic\"]\n    assert result[\"authors\"][0] == {\n        \"name\": \"Thomas Kluyver\",\n        \"email\": \"thomas@kluyver.me.uk\",\n    }\n    assert result[\"urls\"][\"homepage\"] == \"https://github.com/takluyver/flit\"\n    assert result[\"requires-python\"] == \">=3.5\"\n    assert result[\"readme\"] == \"README.rst\"\n    assert result[\"urls\"][\"Documentation\"] == \"https://flit.readthedocs.io/en/latest/\"\n    assert result[\"dependencies\"] == [\n        \"requests>=2.6\",\n        'configparser; python_version == \"2.7\"',\n    ]\n\n    assert result[\"optional-dependencies\"][\"test\"] == [\n        \"pytest >=2.7.3\",\n        \"pytest-cov\",\n    ]\n\n    assert result[\"scripts\"][\"flit\"] == \"flit:main\"\n    assert result[\"entry-points\"][\"pygments.lexers\"][\"dogelang\"] == \"dogelang.lexer:DogeLexer\"\n    build = settings[\"build\"]\n    assert build[\"includes\"] == [\"doc/\"]\n    assert build[\"excludes\"] == [\"doc/*.html\"]\n\n\ndef test_convert_error_preserve_metadata(project):\n    pyproject_file = FIXTURES / \"poetry-error.toml\"\n    try:\n        poetry.convert(project, pyproject_file, ns())\n    except MetaConvertError as e:\n        assert e.data[\"name\"] == \"test-poetry\"\n        assert \"dependencies: Invalid specifier\" in str(e)\n    else:\n        pytest.fail(\"Should raise MetaConvertError\")\n\n\ndef test_import_requirements_with_group(project):\n    golden_file = FIXTURES / \"requirements.txt\"\n    assert requirements.check_fingerprint(project, golden_file)\n    result, settings = requirements.convert(project, golden_file, ns(group=\"test\"))\n\n    group = result[\"optional-dependencies\"][\"test\"]\n    dev_group = settings[\"dev-dependencies\"][\"dev\"]\n    assert \"webassets==2.0\" in group\n    assert 'whoosh==2.7.4; sys_platform == \"win32\"' in group\n    assert \"-e git+https://github.com/pypa/pip.git@main#egg=pip\" not in group\n    assert \"-e git+https://github.com/pypa/pip.git@main#egg=pip\" in dev_group\n    assert not result.get(\"dependencies\")\n\n\ndef test_export_requirements_with_self(project):\n    result = requirements.export(project, [], ns(self=True, hashes=False))\n    assert result.strip().splitlines()[-1] == \".  # this package\"\n\n\ndef test_export_requirements_with_editable_self(project):\n    result = requirements.export(project, [], ns(editable_self=True, hashes=False))\n    assert result.strip().splitlines()[-1] == \"-e .  # this package\"\n\n\ndef test_keep_env_vars_in_source(project, monkeypatch):\n    monkeypatch.setenv(\"USER\", \"foo\")\n    monkeypatch.setenv(\"PASSWORD\", \"bar\")\n    project.pyproject.settings[\"source\"] = [{\"url\": \"https://${USER}:${PASSWORD}@test.pypi.org/simple\", \"name\": \"pypi\"}]\n    result = requirements.export(project, [], ns())\n    assert result.strip().splitlines()[-1] == \"--index-url https://${USER}:${PASSWORD}@test.pypi.org/simple\"\n\n\ndef test_expand_env_vars_in_source(project, monkeypatch):\n    monkeypatch.setenv(\"USER\", \"foo\")\n    monkeypatch.setenv(\"PASSWORD\", \"bar\")\n    project.pyproject.settings[\"source\"] = [{\"url\": \"https://foo:bar@test.pypi.org/simple\", \"name\": \"pypi\"}]\n    result = requirements.export(project, [], ns(expandvars=True))\n    assert result.strip().splitlines()[-1] == \"--index-url https://foo:bar@test.pypi.org/simple\"\n\n\ndef test_export_find_links(project, monkeypatch):\n    url = \"https://storage.googleapis.com/jax-releases/jax_cuda_releases.html\"\n    project.pyproject.settings[\"source\"] = [{\"url\": url, \"name\": \"jax\", \"type\": \"find_links\"}]\n    result = requirements.export(project, [], ns())\n    assert result.strip().splitlines()[-1] == f\"--find-links {url}\"\n\n\ndef test_export_replace_project_root(project):\n    artifact = FIXTURES / \"artifacts/first-2.0.2-py2.py3-none-any.whl\"\n    shutil.copy2(artifact, project.root)\n    with cd(project.root):\n        req = parse_requirement(f\"./{artifact.name}\")\n    result = requirements.export(project, [req], ns(hashes=False))\n    assert \"${PROJECT_ROOT}\" not in result\n\n\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_convert_setup_py_project(project, pdm):\n    project._saved_python = None\n    project.project_config[\"python.use_venv\"] = True\n    pdm([\"add\", \"setuptools\"], obj=project)\n    golden_file = FIXTURES / \"projects/test-setuptools/setup.py\"\n    assert setup_py.check_fingerprint(project, golden_file)\n    result, settings = setup_py.convert(project, golden_file, ns())\n    assert result == {\n        \"name\": \"mymodule\",\n        \"version\": \"0.1.0\",\n        \"description\": \"A test module\",\n        \"keywords\": [\"one\", \"two\"],\n        \"readme\": \"README.md\",\n        \"authors\": [{\"name\": \"frostming\"}],\n        \"license\": {\"text\": \"MIT\"},\n        \"classifiers\": [\"Framework :: Django\", \"Programming Language :: Python :: 3\"],\n        \"requires-python\": \">=3.5\",\n        \"dependencies\": ['importlib-metadata; python_version<\"3.10\"', \"requests\"],\n        \"scripts\": {\"mycli\": \"mymodule:main\"},\n    }\n    assert settings == {\"package-dir\": \"src\"}\n\n\ndef test_convert_poetry_project_with_circular_dependency(project):\n    parent_file = FIXTURES / \"projects/poetry-with-circular-dep/pyproject.toml\"\n    child_file = FIXTURES / \"projects/poetry-with-circular-dep/packages/child/pyproject.toml\"\n\n    _, settings = poetry.convert(project, parent_file, ns())\n    assert settings[\"dev-dependencies\"][\"dev\"] == [\"child @ file:///${PROJECT_ROOT}/packages/child\"]\n\n    _, settings = poetry.convert(project, child_file, ns())\n    assert settings[\"dev-dependencies\"][\"dev\"] == [\"parent @ file:///${PROJECT_ROOT}/../..\"]\n\n\ndef test_export_pylock_toml(core, pdm):\n    project = core.create_project(FIXTURES / \"projects/demo\")\n    golden_file = FIXTURES / \"projects/demo/pylock.toml\"\n    result = pdm([\"export\", \"-f\", \"pylock\"], obj=project, strict=True)\n    assert result.stdout.strip() == golden_file.read_text(encoding=\"utf-8\").strip()\n    with cd(project.root):\n        result = pdm([\"export\", \"-f\", \"pylock\", \"-L\", \"pdm.no_groups.lock\"])\n    assert result.exit_code == 1\n    assert \"inherit_metadata strategy is required for pylock format\" in result.stderr\n\n\ndef test_export_from_pylock_not_empty(core, pdm):\n    \"\"\"Test that exporting from pylock.toml produces non-empty output (fixes issue #3573).\"\"\"\n    project = core.create_project(FIXTURES / \"projects/demo\")\n\n    # Export from pylock.toml to requirements format\n    with cd(project.root):\n        result = pdm([\"export\", \"-f\", \"requirements\", \"-L\", \"pylock.toml\", \"--no-hashes\"], obj=project, strict=True)\n        assert result.exit_code == 0\n\n    # The output should not be empty (this was the original bug)\n    output_lines = [\n        line.strip() for line in result.stdout.strip().split(\"\\n\") if line.strip() and not line.strip().startswith(\"#\")\n    ]\n    assert len(output_lines) > 0, \"Export from pylock.toml should not be empty\"\n\n    # Should contain expected packages\n    output = result.stdout\n    assert any(pkg in output for pkg in [\"chardet\", \"idna\"]), \"Expected at least some packages in output\"\n"
  },
  {
    "path": "tests/test_installer.py",
    "content": "from __future__ import annotations\n\nimport logging\nimport os\nimport venv\nfrom pathlib import Path\nfrom typing import Callable\n\nimport pytest\nfrom unearth import Link\n\nfrom pdm import utils\nfrom pdm.core import Core\nfrom pdm.environments.base import BaseEnvironment\nfrom pdm.environments.local import PythonLocalEnvironment\nfrom pdm.environments.python import PythonEnvironment\nfrom pdm.installers import InstallManager\nfrom pdm.models.cached_package import CachedPackage\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.requirements import parse_requirement\nfrom pdm.project.core import Project\nfrom tests import FIXTURES\n\npytestmark = pytest.mark.usefixtures(\"local_finder\")\n\n\n@pytest.fixture()\ndef supports_link(preferred: str | None, monkeypatch: pytest.MonkeyPatch) -> Callable[[str], bool]:\n    original = utils.fs_supports_link_method\n\n    def mocked_support(linker: str) -> bool:\n        if preferred is None:\n            return False\n        if preferred == \"hardlink\" and linker == \"symlink\":\n            return False\n        return original(linker)\n\n    monkeypatch.setattr(utils, \"fs_supports_link_method\", mocked_support)\n    return mocked_support\n\n\ndef _prepare_project_for_env(project: Project, env_cls: type[BaseEnvironment]):\n    project._saved_python = None\n    project._python = None\n    if env_cls is PythonEnvironment:\n        venv.create(project.root / \".venv\", symlinks=True)\n        project.project_config[\"python.use_venv\"] = True\n\n\n@pytest.fixture(params=(PythonEnvironment, PythonLocalEnvironment), autouse=True)\ndef environment(request: pytest.RequestFixture, project: Project) -> type[BaseEnvironment]:\n    # Run all test against all environments as installation and cache behavior may differ\n    env_cls: type[BaseEnvironment] = request.param\n    _prepare_project_for_env(project, env_cls)\n    return env_cls\n\n\ndef test_install_wheel_with_inconsistent_dist_info(project):\n    req = parse_requirement(\"pyfunctional\")\n    candidate = Candidate(\n        req,\n        link=Link(\"http://fixtures.test/artifacts/PyFunctional-1.4.3-py3-none-any.whl\"),\n    )\n    installer = InstallManager(project.environment)\n    installer.install(candidate)\n    assert \"pyfunctional\" in project.environment.get_working_set()\n\n\ndef test_install_with_file_existing(project):\n    req = parse_requirement(\"demo\")\n    candidate = Candidate(\n        req,\n        link=Link(\"http://fixtures.test/artifacts/demo-0.0.1-py2.py3-none-any.whl\"),\n    )\n    lib_path = project.environment.get_paths()[\"purelib\"]\n    os.makedirs(lib_path, exist_ok=True)\n    with open(os.path.join(lib_path, \"demo.py\"), \"w\") as fp:\n        fp.write(\"print('hello')\\n\")\n    installer = InstallManager(project.environment)\n    installer.install(candidate)\n\n\ndef test_uninstall_commit_rollback(project):\n    req = parse_requirement(\"demo\")\n    candidate = Candidate(\n        req,\n        link=Link(\"http://fixtures.test/artifacts/demo-0.0.1-py2.py3-none-any.whl\"),\n    )\n    installer = InstallManager(project.environment)\n    lib_path = project.environment.get_paths()[\"purelib\"]\n    installer.install(candidate)\n    lib_file = os.path.join(lib_path, \"demo.py\")\n    assert os.path.exists(lib_file)\n    remove_paths = installer.get_paths_to_remove(project.environment.get_working_set()[\"demo\"])\n    remove_paths.remove()\n    assert not os.path.exists(lib_file)\n    remove_paths.rollback()\n    assert os.path.exists(lib_file)\n\n\ndef test_rollback_after_commit(project, caplog):\n    caplog.set_level(logging.ERROR, logger=\"pdm.termui\")\n    logging.getLogger(\"pdm.termui\").addHandler(caplog.handler)\n    req = parse_requirement(\"demo\")\n    candidate = Candidate(\n        req,\n        link=Link(\"http://fixtures.test/artifacts/demo-0.0.1-py2.py3-none-any.whl\"),\n    )\n    installer = InstallManager(project.environment)\n    lib_path = project.environment.get_paths()[\"purelib\"]\n    installer.install(candidate)\n    lib_file = os.path.join(lib_path, \"demo.py\")\n    assert os.path.exists(lib_file)\n    remove_paths = installer.get_paths_to_remove(project.environment.get_working_set()[\"demo\"])\n    remove_paths.remove()\n    remove_paths.commit()\n    assert not os.path.exists(lib_file)\n    caplog.clear()\n    remove_paths.rollback()\n    assert not os.path.exists(lib_file)\n\n    assert any(message == \"Can't rollback, not uninstalled yet\" for message in caplog.messages)\n\n\n@pytest.mark.parametrize(\"use_install_cache\", [False, True])\ndef test_uninstall_with_console_scripts(project, use_install_cache):\n    req = parse_requirement(\"celery\")\n    candidate = Candidate(\n        req,\n        link=Link(\"http://fixtures.test/artifacts/celery-4.4.2-py2.py3-none-any.whl\"),\n    )\n    installer = InstallManager(project.environment, use_install_cache=use_install_cache)\n    installer.install(candidate)\n    celery_script = os.path.join(\n        project.environment.get_paths()[\"scripts\"],\n        \"celery.exe\" if os.name == \"nt\" else \"celery\",\n    )\n    assert os.path.exists(celery_script)\n    installer.uninstall(project.environment.get_working_set()[\"celery\"])\n    assert not os.path.exists(celery_script)\n\n\n@pytest.mark.parametrize(\"preferred\", [\"symlink\", \"hardlink\", None])\ndef test_install_wheel_with_cache(project, pdm, supports_link):\n    req = parse_requirement(\"future-fstrings\")\n    candidate = Candidate(\n        req,\n        link=Link(\"http://fixtures.test/artifacts/future_fstrings-1.2.0-py2.py3-none-any.whl\"),\n    )\n    installer = InstallManager(project.environment, use_install_cache=True)\n    installer.install(candidate)\n\n    lib_path = project.environment.get_paths()[\"purelib\"]\n    if supports_link(\"symlink\"):\n        assert os.path.islink(os.path.join(lib_path, \"future_fstrings.py\"))\n        assert os.path.islink(os.path.join(lib_path, \"aaaaa_future_fstrings.pth\"))\n    else:\n        assert os.path.isfile(os.path.join(lib_path, \"future_fstrings.py\"))\n        assert os.path.isfile(os.path.join(lib_path, \"aaaaa_future_fstrings.pth\"))\n\n    for file in CachedPackage.cache_files:\n        assert not os.path.exists(os.path.join(lib_path, file))\n\n    cache_name = \"future_fstrings-1.2.0-py2.py3-none-any.whl.cache\"\n    assert any(p.path.name == cache_name for p in project.package_cache.iter_packages())\n    pdm([\"run\", \"python\", \"-m\", \"site\"], object=project)\n    r = pdm([\"run\", \"python\", \"-c\", \"import future_fstrings\"], obj=project)\n    assert r.exit_code == 0\n    pdm([\"cache\", \"clear\", \"packages\"], obj=project, strict=True)\n    assert supports_link(\"symlink\") is any(p.path.name == cache_name for p in project.package_cache.iter_packages())\n\n    dist = project.environment.get_working_set()[\"future-fstrings\"]\n    installer.uninstall(dist)\n    assert not os.path.exists(os.path.join(lib_path, \"future_fstrings.py\"))\n    assert not os.path.exists(os.path.join(lib_path, \"aaaaa_future_fstrings.pth\"))\n    assert not dist.read_text(\"direct_url.json\")\n\n    pdm([\"cache\", \"clear\", \"packages\"], obj=project, strict=True)\n    assert not any(p.path.name == cache_name for p in project.package_cache.iter_packages())\n\n\n@pytest.mark.parametrize(\"preferred\", [\"symlink\", \"hardlink\", None])\ndef test_can_install_wheel_with_cache_in_multiple_projects(\n    project: Project, core: Core, supports_link, tmp_path_factory, environment\n):\n    projects = []\n\n    for idx in range(3):\n        path: Path = tmp_path_factory.mktemp(f\"project-{idx}\")\n        p = core.create_project(path, global_config=project.global_config.config_file)\n        _prepare_project_for_env(p, environment)\n        projects.append(p)\n\n    req = parse_requirement(\"future-fstrings\")\n    candidate = Candidate(\n        req,\n        link=Link(\"http://fixtures.test/artifacts/future_fstrings-1.2.0-py2.py3-none-any.whl\"),\n    )\n\n    for p in projects:\n        installer = InstallManager(p.environment, use_install_cache=True)\n        installer.install(candidate)\n\n        lib_path = p.environment.get_paths()[\"purelib\"]\n        if supports_link(\"symlink\"):\n            assert os.path.islink(os.path.join(lib_path, \"future_fstrings.py\"))\n            assert os.path.islink(os.path.join(lib_path, \"aaaaa_future_fstrings.pth\"))\n        else:\n            assert os.path.isfile(os.path.join(lib_path, \"future_fstrings.py\"))\n            assert os.path.isfile(os.path.join(lib_path, \"aaaaa_future_fstrings.pth\"))\n\n        for file in CachedPackage.cache_files:\n            assert not os.path.exists(os.path.join(lib_path, file))\n\n\ndef test_url_requirement_is_not_cached(project):\n    req = parse_requirement(\n        \"future-fstrings @ http://fixtures.test/artifacts/future_fstrings-1.2.0-py2.py3-none-any.whl\"\n    )\n    candidate = Candidate(req)\n    installer = InstallManager(project.environment, use_install_cache=True)\n    installer.install(candidate)\n    cache_path = project.cache(\"packages\") / \"future_fstrings-1.2.0-py2.py3-none-any\"\n    assert not cache_path.is_dir()\n    lib_path = project.environment.get_paths()[\"purelib\"]\n    assert os.path.isfile(os.path.join(lib_path, \"future_fstrings.py\"))\n    assert os.path.isfile(os.path.join(lib_path, \"aaaaa_future_fstrings.pth\"))\n    dist = project.environment.get_working_set()[\"future-fstrings\"]\n    assert dist.read_text(\"direct_url.json\")\n\n\ndef test_editable_is_not_cached(project, tmp_path_factory):\n    editable_path: Path = tmp_path_factory.mktemp(\"editable-project\")\n\n    editable_setup = editable_path / \"setup.py\"\n    editable_setup.write_text(\"\"\"\nfrom setuptools import setup\n\nsetup(name='editable-project',\n      version='0.1.0',\n      description='',\n      py_modules=['module'],\n)\n\"\"\")\n    editable_module = editable_path / \"module.py\"\n    editable_module.write_text(\"\")\n\n    req = parse_requirement(f\"{editable_path.as_uri()}#egg=editable-project\", True)\n    candidate = Candidate(req)\n    installer = InstallManager(project.environment, use_install_cache=True)\n    installer.install(candidate)\n\n    cache_path = project.cache(\"packages\") / \"editable_project-0.1.0-0.editable-py3-none-any.whl.cache\"\n    assert not cache_path.is_dir()\n    lib_path = Path(project.environment.get_paths()[\"purelib\"])\n    for pth in lib_path.glob(\"*editable_project*.pth\"):\n        assert pth.is_file()\n        assert not pth.is_symlink()\n\n\n@pytest.mark.parametrize(\"use_install_cache\", [False, True])\ndef test_install_wheel_with_data_scripts(project, use_install_cache):\n    req = parse_requirement(\"jmespath\")\n    candidate = Candidate(\n        req,\n        link=Link(\"http://fixtures.test/artifacts/jmespath-0.10.0-py2.py3-none-any.whl\"),\n    )\n    installer = InstallManager(project.environment, use_install_cache=use_install_cache)\n    installer.install(candidate)\n    bin_path = os.path.join(project.environment.get_paths()[\"scripts\"], \"jp.py\")\n    assert os.path.isfile(bin_path)\n    if os.name != \"nt\":\n        assert os.stat(bin_path).st_mode & 0o100\n\n    dist = project.environment.get_working_set()[\"jmespath\"]\n    installer.uninstall(dist)\n    assert not os.path.exists(bin_path)\n\n\ndef test_compress_file_list_for_rename():\n    from pdm.installers.uninstallers import compress_for_rename\n\n    project_root = str(FIXTURES / \"projects\")\n\n    paths = {\n        \"test-removal/subdir\",\n        \"test-removal/subdir/__init__.py\",\n        \"test-removal/__init__.py\",\n        \"test-removal/bar.py\",\n        \"test-removal/foo.py\",\n        \"test-removal/non_exist.py\",\n    }\n    abs_paths = {os.path.join(project_root, path) for path in paths}\n    assert sorted(compress_for_rename(abs_paths)) == [os.path.join(project_root, \"test-removal\" + os.sep)]\n"
  },
  {
    "path": "tests/test_integration.py",
    "content": "import findpython\nimport pytest\n\nfrom pdm.utils import cd\n\nDEFAULT_PYTHON_VERSIONS = [\"3.9\", \"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]\nPYPROJECT = {\n    \"project\": {\"name\": \"test-project\", \"version\": \"0.1.0\", \"requires-python\": \">=3.7\"},\n    \"build-system\": {\"requires\": [\"pdm-backend\"], \"build-backend\": \"pdm.backend\"},\n}\n\n\ndef get_python_versions():\n    finder = findpython.Finder(resolve_symlinks=True)\n    available_versions = []\n    for version in DEFAULT_PYTHON_VERSIONS:\n        v = finder.find(version, allow_prereleases=True)\n        if v and v.is_valid():\n            available_versions.append(version)\n    return available_versions\n\n\nPYTHON_VERSIONS = get_python_versions()\n\n\n@pytest.mark.integration\n@pytest.mark.network\n@pytest.mark.flaky(reruns=3)\n@pytest.mark.parametrize(\"python_version\", PYTHON_VERSIONS)\ndef test_basic_integration(python_version, core, tmp_path, pdm):\n    \"\"\"An e2e test case to ensure PDM works on all supported Python versions\"\"\"\n    project = core.create_project(tmp_path)\n    project.project_config[\"python.use_venv\"] = True\n    project.pyproject.set_data(PYPROJECT)\n    project.root.joinpath(\"foo.py\").write_text(\"import django\\n\")\n    project._environment = None\n    pdm([\"use\", \"-f\", python_version], obj=project, strict=True, cleanup=False)\n    pdm([\"add\", \"django\", \"-v\"], obj=project, strict=True, cleanup=False)\n    with cd(project.root):\n        pdm([\"run\", \"python\", \"foo.py\"], obj=project, strict=True, cleanup=False)\n        pdm([\"build\", \"-v\"], obj=project, strict=True, cleanup=False)\n    pdm([\"remove\", \"-v\", \"django\"], obj=project, strict=True, cleanup=False)\n    result = pdm([\"list\"], obj=project, strict=True)\n    assert not any(line.strip().lower().startswith(\"django\") for line in result.output.splitlines())\n\n\n@pytest.mark.integration\n@pytest.mark.skipif(len(PYTHON_VERSIONS) < 2, reason=\"Need at least 2 Python versions to test\")\ndef test_use_python_write_file(pdm, project):\n    pdm([\"use\", PYTHON_VERSIONS[0]], obj=project, strict=True)\n    assert f\"{project.python.major}.{project.python.minor}\" == PYTHON_VERSIONS[0]\n    assert project.root.joinpath(\".python-version\").read_text().strip() == PYTHON_VERSIONS[0]\n    pdm([\"use\", PYTHON_VERSIONS[1]], obj=project, strict=True)\n    assert f\"{project.python.major}.{project.python.minor}\" == PYTHON_VERSIONS[1]\n    assert project.root.joinpath(\".python-version\").read_text().strip() == PYTHON_VERSIONS[1]\n\n\n@pytest.mark.integration\n@pytest.mark.skipif(len(PYTHON_VERSIONS) < 2, reason=\"Need at least 2 Python versions to test\")\ndef test_use_python_write_no_file(pdm, project):\n    project.project_config[\"python.use_python_version\"] = False\n    pdm([\"use\", PYTHON_VERSIONS[0]], obj=project, strict=True)\n    assert f\"{project.python.major}.{project.python.minor}\" == PYTHON_VERSIONS[0]\n    assert not project.root.joinpath(\".python-version\").exists()\n    project.project_config[\"python.use_python_version\"] = True\n    pdm([\"use\", PYTHON_VERSIONS[1]], obj=project, strict=True)\n    assert f\"{project.python.major}.{project.python.minor}\" == PYTHON_VERSIONS[1]\n    assert project.root.joinpath(\".python-version\").exists()\n    assert project.root.joinpath(\".python-version\").read_text().strip() == PYTHON_VERSIONS[1]\n\n\n@pytest.mark.integration\n@pytest.mark.network\n@pytest.mark.parametrize(\"python_version\", PYTHON_VERSIONS)\n@pytest.mark.parametrize(\"via_env\", [True, False])\ndef test_init_project_respect_version_file(pdm, project, python_version, via_env, monkeypatch):\n    project.project_config[\"python.use_venv\"] = True\n    if via_env:\n        monkeypatch.setenv(\"PDM_PYTHON_VERSION\", python_version)\n    else:\n        project.root.joinpath(\".python-version\").write_text(python_version)\n    project._saved_python = None\n    project._environment = None\n    pdm([\"install\"], obj=project, strict=True)\n    assert f\"{project.python.major}.{project.python.minor}\" == python_version\n\n\n@pytest.mark.integration\n@pytest.mark.network\n@pytest.mark.parametrize(\"python_version\", PYTHON_VERSIONS)\ndef test_use_python_write_file_multiple_versions(pdm, project, python_version, monkeypatch):\n    no_versions = [p for p in DEFAULT_PYTHON_VERSIONS if p not in PYTHON_VERSIONS]\n    project.project_config[\"python.use_venv\"] = True\n\n    if no_versions and PYTHON_VERSIONS:\n        version_content = f\"{no_versions[0]}\\n{PYTHON_VERSIONS[0]}\"\n    elif no_versions:\n        version_content = \"\\n\".join(no_versions)\n    else:\n        version_content = \"\\n\".join(PYTHON_VERSIONS[:2])\n\n    project.root.joinpath(\".python-version\").write_text(version_content)\n    project._saved_python = None\n    project._environment = None\n    pdm([\"install\"], obj=project, strict=True)\n\n    assert f\"{project.python.major}.{project.python.minor}\" in PYTHON_VERSIONS\n\n\n@pytest.mark.integration\n@pytest.mark.skipif(len(PYTHON_VERSIONS) < 2, reason=\"Need at least 2 Python versions to test\")\ndef test_use_python_write_file_with_use_python_version(pdm, project, monkeypatch):\n    configured_python_version = PYTHON_VERSIONS[0]\n    project.project_config[\"python.use_venv\"] = True\n    project.project_config[\"python.use_python_version\"] = True\n    project.root.joinpath(\".python-version\").write_text(configured_python_version)\n    project._saved_python = None\n    project._environment = None\n    pdm([\"install\"], obj=project, strict=True)\n    assert f\"{project.python.major}.{project.python.minor}\" == configured_python_version\n\n\n@pytest.mark.integration\n@pytest.mark.skipif(len(PYTHON_VERSIONS) < 2, reason=\"Need at least 2 Python versions to test\")\ndef test_use_python_write_file_without_use_python_version(pdm, project):\n    project.project_config[\"python.use_venv\"] = True\n    project.project_config[\"python.use_python_version\"] = False\n    project.root.joinpath(\".python-version\").write_text(PYTHON_VERSIONS[0])\n    project._saved_python = None\n    project._environment = None\n    pdm([\"install\"], obj=project, strict=True)\n    assert f\"{project.python.major}.{project.python.minor}\" in PYTHON_VERSIONS\n\n\ndef test_actual_list_freeze(project, local_finder, pdm):\n    pdm([\"config\", \"-l\", \"install.parallel\", \"false\"], obj=project, strict=True)\n    pdm([\"add\", \"first\"], obj=project, strict=True)\n    r = pdm([\"list\", \"--freeze\"], obj=project)\n    assert \"first==2.0.2\" in r.output\n"
  },
  {
    "path": "tests/test_plugin.py",
    "content": "import sys\nfrom unittest import mock\n\nimport pytest\n\nfrom pdm.cli.commands.base import BaseCommand\nfrom pdm.compat import importlib_metadata\nfrom pdm.project.config import ConfigItem\nfrom pdm.utils import cd\n\n\nclass HelloCommand(BaseCommand):\n    def add_arguments(self, parser) -> None:\n        parser.add_argument(\"-n\", \"--name\", help=\"The person's name\")\n\n    def handle(self, project, options) -> None:\n        greeting = \"Hello world\"\n        if options.name:\n            greeting = f\"Hello, {options.name}\"\n        print(greeting)\n\n\ndef new_command(core):\n    core.register_command(HelloCommand, \"hello\")\n\n\ndef replace_command(core):\n    core.register_command(HelloCommand, \"info\")\n\n\ndef add_new_config(core):\n    core.add_config(\"foo\", ConfigItem(\"Test config\", \"bar\"))\n\n\ndef make_entry_point(plugin):\n    ret = mock.Mock()\n    ret.load.return_value = plugin\n    return ret\n\n\ndef test_plugin_new_command(pdm, mocker, project, core):\n    mocker.patch.object(\n        importlib_metadata,\n        \"entry_points\",\n        return_value=[make_entry_point(new_command)],\n    )\n    core.init_parser()\n    core.load_plugins()\n    result = pdm([\"--help\"], obj=project)\n    assert \"hello\" in result.output\n\n    result = pdm([\"hello\"], obj=project)\n    assert result.output.strip() == \"Hello world\"\n\n    result = pdm([\"hello\", \"-n\", \"Frost\"], obj=project)\n    assert result.output.strip() == \"Hello, Frost\"\n\n\ndef test_plugin_replace_command(pdm, mocker, project, core):\n    mocker.patch.object(\n        importlib_metadata,\n        \"entry_points\",\n        return_value=[make_entry_point(replace_command)],\n    )\n    core.init_parser()\n    core.load_plugins()\n\n    result = pdm([\"info\"], obj=project)\n    assert result.output.strip() == \"Hello world\"\n\n    result = pdm([\"info\", \"-n\", \"Frost\"], obj=project)\n    assert result.output.strip() == \"Hello, Frost\"\n\n\ndef test_load_multiple_plugins(pdm, mocker, core):\n    mocker.patch.object(\n        importlib_metadata,\n        \"entry_points\",\n        return_value=[make_entry_point(new_command), make_entry_point(add_new_config)],\n    )\n    core.init_parser()\n    core.load_plugins()\n\n    result = pdm([\"hello\"])\n    assert result.output.strip() == \"Hello world\", result.outputs\n\n    result = pdm([\"config\", \"foo\"])\n    assert result.output.strip() == \"bar\"\n\n\ndef test_old_entry_point_compatibility(pdm, mocker, core):\n    def get_entry_points(group):\n        if group == \"pdm\":\n            return [make_entry_point(new_command)]\n        if group == \"pdm.plugin\":\n            return [make_entry_point(add_new_config)]\n        return []\n\n    mocker.patch.object(importlib_metadata, \"entry_points\", side_effect=get_entry_points)\n    core.init_parser()\n    core.load_plugins()\n\n    result = pdm([\"hello\"])\n    assert result.output.strip() == \"Hello world\"\n\n    result = pdm([\"config\", \"foo\"])\n    assert result.output.strip() == \"bar\"\n\n\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_project_plugin_library(pdm, project, core, monkeypatch):\n    monkeypatch.setattr(sys, \"path\", sys.path[:])\n    project.pyproject.settings[\"plugins\"] = [\"pdm-hello\"]\n    pdm([\"install\", \"--plugins\"], obj=project, strict=True)\n    assert project.root.joinpath(\".pdm-plugins\").exists()\n    assert \"pdm-hello\" not in project.environment.get_working_set()\n    with cd(project.root):\n        core.load_plugins()\n        result = pdm([\"hello\", \"Frost\"], strict=True)\n    assert result.stdout.strip() == \"Hello, Frost!\"\n\n\n@pytest.mark.parametrize(\n    \"req_str\",\n    [\n        \"-e file:///${PROJECT_ROOT}/plugins/test-plugin-pdm\",\n        \"-e test_plugin_pdm @ file:///${PROJECT_ROOT}/plugins/test-plugin-pdm\",\n    ],\n)\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_install_local_plugin_without_name(pdm, project, core, req_str):\n    import shutil\n\n    from . import FIXTURES\n\n    test_plugin_path = FIXTURES / \"projects\" / \"test-plugin-pdm\"\n    project.root.joinpath(\"plugins\").mkdir(exist_ok=True)\n    shutil.copytree(test_plugin_path, project.root / \"plugins\" / \"test-plugin-pdm\", dirs_exist_ok=True)\n\n    project.pyproject.settings[\"plugins\"] = [req_str]\n    project.pyproject.write()\n    with cd(project.root):\n        result = pdm([\"install\", \"--plugins\", \"-vv\"], obj=project, strict=True)\n\n        assert project.root.joinpath(\".pdm-plugins\").exists()\n        core.load_plugins()\n        result = pdm([\"hello\", \"--name\", \"Frost\"], strict=True)\n    assert result.stdout.strip() == \"Hello, Frost\"\n"
  },
  {
    "path": "tests/test_project.py",
    "content": "from __future__ import annotations\n\nimport os\nimport sys\nimport venv\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nimport pytest\nfrom pbs_installer import PythonVersion\nfrom pytest_httpserver import HTTPServer\n\nfrom pdm.environments import PythonEnvironment\nfrom pdm.exceptions import PdmException, ProjectError\nfrom pdm.models.requirements import parse_requirement\nfrom pdm.models.specifiers import PySpecSet\nfrom pdm.models.venv import get_venv_python\nfrom pdm.utils import cd, is_path_relative_to, parse_version\n\nif TYPE_CHECKING:\n    from pdm.project.core import Project\n    from pdm.pytest import PDMCallable\n\nPYTHON_VERSIONS = [\"3.9.7\", \"3.10.12\", \"3.10.11\", \"3.9.0\", \"3.10.13\", \"3.9.12\"]\n\n\ndef get_python_versions() -> list[PythonVersion]:\n    python_versions = []\n    for v in PYTHON_VERSIONS:\n        major, minor, micro = v.split(\".\")\n        python_versions.append(PythonVersion(\"cpython\", int(major), int(minor), int(micro)))\n    return python_versions\n\n\ndef test_project_python_with_pyenv_support(project, mocker, monkeypatch):\n    project._saved_python = None\n    project._python = None\n    monkeypatch.setenv(\"PDM_IGNORE_SAVED_PYTHON\", \"1\")\n    mocker.patch(\"pdm.project.core.PYENV_ROOT\", str(project.root))\n    pyenv_python = project.root / \"shims/python\"\n    if os.name == \"nt\":\n        pyenv_python = pyenv_python.with_suffix(\".bat\")\n    pyenv_python.parent.mkdir()\n    pyenv_python.touch()\n    mocker.patch(\n        \"findpython.python.PythonVersion._get_version\",\n        return_value=parse_version(\"3.9.0\"),\n    )\n    mocker.patch(\"findpython.python.PythonVersion._get_interpreter\", return_value=sys.executable)\n    assert Path(project.python.path) == pyenv_python\n    assert project.python.executable == Path(sys.executable)\n\n    # Clean cache\n    project._python = None\n\n    project.project_config[\"python.use_pyenv\"] = False\n    assert Path(project.python.path) != pyenv_python\n\n\ndef test_project_config_items(project):\n    config = project.config\n\n    for item in (\"python.use_pyenv\", \"pypi.url\", \"cache_dir\"):\n        assert item in config\n\n\ndef test_project_config_set_invalid_key(project):\n    config = project.project_config\n\n    with pytest.raises(KeyError):\n        config[\"foo\"] = \"bar\"\n\n\ndef test_project_sources_overriding_pypi(project):\n    project.project_config[\"pypi.url\"] = \"https://test.pypi.org/simple\"\n    assert project.sources[0].url == \"https://test.pypi.org/simple\"\n\n    project.pyproject.settings[\"source\"] = [{\"url\": \"https://example.org/simple\", \"name\": \"pypi\", \"verify_ssl\": True}]\n    assert project.sources[0].url == \"https://example.org/simple\"\n\n\ndef test_project_sources_env_var_expansion(project, monkeypatch):\n    monkeypatch.setenv(\"PYPI_USER\", \"user\")\n    monkeypatch.setenv(\"PYPI_PASS\", \"password\")\n    project.project_config[\"pypi.url\"] = \"https://${PYPI_USER}:${PYPI_PASS}@test.pypi.org/simple\"\n    # expanded in sources\n    assert project.sources[0].url == \"https://user:password@test.pypi.org/simple\"\n    # not expanded in project config\n    assert project.project_config[\"pypi.url\"] == \"https://${PYPI_USER}:${PYPI_PASS}@test.pypi.org/simple\"\n\n    project.pyproject.settings[\"source\"] = [\n        {\n            \"url\": \"https://${PYPI_USER}:${PYPI_PASS}@example.org/simple\",\n            \"name\": \"pypi\",\n            \"verify_ssl\": True,\n        }\n    ]\n    # expanded in sources\n    assert project.sources[0].url == \"https://user:password@example.org/simple\"\n    # not expanded in tool settings\n    assert project.pyproject.settings[\"source\"][0][\"url\"] == \"https://${PYPI_USER}:${PYPI_PASS}@example.org/simple\"\n\n    project.pyproject.settings[\"source\"] = [\n        {\n            \"url\": \"https://${PYPI_USER}:${PYPI_PASS}@example2.org/simple\",\n            \"name\": \"example2\",\n            \"verify_ssl\": True,\n        }\n    ]\n    # expanded in sources\n    assert project.sources[1].url == \"https://user:password@example2.org/simple\"\n    # not expanded in tool settings\n    assert project.pyproject.settings[\"source\"][0][\"url\"] == \"https://${PYPI_USER}:${PYPI_PASS}@example2.org/simple\"\n\n\ndef test_global_project(tmp_path, core):\n    project = core.create_project(tmp_path, True)\n    assert project.is_global\n    assert isinstance(project.environment, PythonEnvironment)\n\n\ndef test_auto_global_project(tmp_path, core):\n    tmp_path.joinpath(\".pdm-home\").mkdir()\n    (tmp_path / \".pdm-home/config.toml\").write_text(\"[global_project]\\nfallback = true\\n\")\n    with cd(tmp_path):\n        project = core.create_project(global_config=tmp_path / \".pdm-home/config.toml\")\n    assert project.is_global\n\n\ndef test_project_use_venv(project):\n    project._saved_python = None\n    project._python = None\n    scripts = \"Scripts\" if os.name == \"nt\" else \"bin\"\n    suffix = \".exe\" if os.name == \"nt\" else \"\"\n    venv.create(project.root / \"venv\", symlinks=True)\n\n    project.project_config[\"python.use_venv\"] = True\n    env = project.environment\n    assert env.interpreter.executable == project.root / \"venv\" / scripts / f\"python{suffix}\"\n    assert not env.is_local\n\n\ndef test_project_packages_path(project):\n    packages_path = project.environment.packages_path\n    version = \".\".join(map(str, sys.version_info[:2]))\n    if os.name == \"nt\" and sys.maxsize <= 2**32:\n        assert packages_path.name == version + \"-32\"\n    else:\n        assert packages_path.name == version\n\n\ndef test_project_auto_detect_venv(project):\n    venv.create(project.root / \"test_venv\")\n\n    scripts = \"Scripts\" if os.name == \"nt\" else \"bin\"\n    suffix = \".exe\" if os.name == \"nt\" else \"\"\n\n    project.project_config[\"python.use_venv\"] = True\n    project._python = None\n    project._saved_python = (project.root / \"test_venv\" / scripts / f\"python{suffix}\").as_posix()\n\n    assert not project.environment.is_local\n\n\n@pytest.mark.path\ndef test_ignore_saved_python(project, monkeypatch):\n    project.project_config[\"python.use_venv\"] = True\n    project._python = None\n    scripts = \"Scripts\" if os.name == \"nt\" else \"bin\"\n    suffix = \".exe\" if os.name == \"nt\" else \"\"\n    venv.create(project.root / \"venv\", symlinks=True)\n    monkeypatch.setenv(\"PDM_IGNORE_SAVED_PYTHON\", \"1\")\n    assert project.python.executable != project._saved_python\n    assert project.python.executable == project.root / \"venv\" / scripts / f\"python{suffix}\"\n\n\ndef test_select_dependencies(project):\n    project.pyproject.metadata[\"dependencies\"] = [\"requests\"]\n    project.pyproject.metadata[\"optional-dependencies\"] = {\n        \"security\": [\"cryptography\"],\n        \"venv\": [\"virtualenv\"],\n    }\n    project.pyproject.dependency_groups.update(\n        {\n            \"test\": [\"pytest\"],\n            \"doc\": [\"mkdocs\"],\n            \"all\": [{\"include-group\": \"test\"}, {\"include-group\": \"doc\"}],\n        }\n    )\n    assert sorted([r.key for r in project.get_dependencies()]) == [\"requests\"]\n    assert sorted([r.key for r in project.get_dependencies(\"security\")]) == [\"cryptography\"]\n    assert sorted([r.key for r in project.get_dependencies(\"test\")]) == [\"pytest\"]\n    assert sorted([r.key for r in project.get_dependencies(\"all\")]) == [\"mkdocs\", \"pytest\"]\n    assert sorted(project.iter_groups()) == [\n        \"all\",\n        \"default\",\n        \"doc\",\n        \"security\",\n        \"test\",\n        \"venv\",\n    ]\n\n\ndef test_invalid_dependency_group(project):\n    project.pyproject.dependency_groups.update(\n        {\n            \"invalid\": [{\"invalid-key\": True}],\n            \"missing\": [{\"include-group\": \"missing-group\"}],\n            \"doc\": [\"mkdocs\"],\n            \"recursive\": [{\"include-group\": \"invalid\"}],\n        }\n    )\n    assert sorted([r.key for r in project.get_dependencies(\"doc\")]) == [\"mkdocs\"]\n    with pytest.raises(ProjectError, match=\"Invalid dependency group item\"):\n        project.get_dependencies(\"invalid\")\n    with pytest.raises(ProjectError, match=\"Invalid dependency group item\"):\n        project.get_dependencies(\"recursive\")\n    with pytest.raises(ProjectError, match=\"Missing group 'missing-group'\"):\n        project.get_dependencies(\"missing\")\n\n\n@pytest.mark.path\ndef test_set_non_exist_python_path(project_no_init):\n    project_no_init._saved_python = \"non-exist-python\"\n    project_no_init._python = None\n    assert project_no_init.python.executable.name != \"non-exist-python\"\n\n\n@pytest.mark.usefixtures(\"venv_backends\")\ndef test_create_venv_first_time(pdm, project, local_finder):\n    project.project_config.update({\"venv.in_project\": False})\n    project._saved_python = None\n    result = pdm([\"install\"], obj=project)\n    assert result.exit_code == 0\n    venv_parent = project.root / \"venvs\"\n    venv_path = next(venv_parent.iterdir(), None)\n    assert venv_path is not None\n\n    assert Path(project._saved_python).relative_to(venv_path)\n\n\n@pytest.mark.usefixtures(\"venv_backends\", \"local_finder\")\n@pytest.mark.parametrize(\"with_pip\", [True, False])\ndef test_create_venv_in_project(pdm, project, with_pip):\n    project.project_config.update({\"venv.in_project\": True, \"venv.with_pip\": with_pip})\n    project._saved_python = None\n    result = pdm([\"install\"], obj=project)\n    assert result.exit_code == 0\n    assert project.root.joinpath(\".venv\").exists()\n    working_set = project.environment.get_working_set()\n    assert (\"pip\" in working_set) is with_pip\n\n\n@pytest.mark.usefixtures(\"venv_backends\")\ndef test_find_interpreters_from_venv(pdm, project, local_finder):\n    project.project_config.update({\"venv.in_project\": False})\n    project._saved_python = None\n    result = pdm([\"install\"], obj=project)\n    assert result.exit_code == 0\n    venv_parent = project.root / \"venvs\"\n    venv_path = next(venv_parent.iterdir(), None)\n    venv_python = get_venv_python(venv_path)\n\n    assert any(venv_python == p.executable for p in project.find_interpreters())\n\n\n@pytest.mark.usefixtures(\"local_finder\")\ndef test_find_interpreters_without_duplicate_relative_paths(pdm, project):\n    project._saved_python = None\n    venv.create(project.root / \".venv\", clear=True)\n    with cd(project.root):\n        bin_dir = \"Scripts\" if os.name == \"nt\" else \"bin\"\n        suffix = \".exe\" if os.name == \"nt\" else \"\"\n        found = list(project.find_interpreters(f\".venv/{bin_dir}/python{suffix}\"))\n        assert len(found) == 1\n\n\ndef test_iter_project_venvs(project):\n    from pdm.cli.commands.venv.utils import get_venv_prefix, iter_venvs\n    from pdm.models.venv import get_venv_python\n\n    venv_parent = Path(project.config[\"venv.location\"])\n    venv_prefix = get_venv_prefix(project)\n    for name in (\"foo\", \"bar\", \"baz\"):\n        venv_python = get_venv_python(venv_parent / (venv_prefix + name))\n        venv_python.parent.mkdir(parents=True)\n        venv_python.touch()\n    dot_venv_python = get_venv_python(project.root / \".venv\")\n    dot_venv_python.parent.mkdir(parents=True)\n    dot_venv_python.touch()\n    venv_keys = [key for key, _ in iter_venvs(project)]\n    assert sorted(venv_keys) == [\"bar\", \"baz\", \"foo\", \"in-project\"]\n\n\ndef test_load_extra_sources(project):\n    project.pyproject.settings[\"source\"] = [\n        {\n            \"name\": \"custom\",\n            \"url\": \"https://custom.pypi.org/simple\",\n        }\n    ]\n    project.global_config[\"pypi.extra.url\"] = \"https://extra.pypi.org/simple\"\n    sources = project.sources\n    assert len(sources) == 3\n    assert [item.name for item in sources] == [\"pypi\", \"custom\", \"extra\"]\n\n    project.global_config[\"pypi.ignore_stored_index\"] = True\n    sources = project.sources\n    assert len(sources) == 1\n    assert [item.name for item in sources] == [\"custom\"]\n\n\ndef test_no_index_raise_error(project):\n    project.global_config[\"pypi.ignore_stored_index\"] = True\n    with pytest.raises(PdmException, match=\"You must specify at least one index\"):\n        with project.environment.get_finder():\n            pass\n\n\ndef test_access_index_with_auth(project, httpserver: HTTPServer):\n    httpserver.expect_request(\n        \"/simple/my-package\", method=\"GET\", headers={\"Authorization\": \"Basic Zm9vOmJhcg==\"}\n    ).respond_with_data(\"OK\")\n    project.global_config.update(\n        {\n            \"pypi.extra.url\": httpserver.url_for(\"/simple\"),\n            \"pypi.extra.username\": \"foo\",\n            \"pypi.extra.password\": \"bar\",\n        }\n    )\n    session = project.environment.session\n    resp = session.get(httpserver.url_for(\"/simple/my-package\"))\n    assert resp.is_success\n\n\ndef test_configured_source_overwriting(project):\n    project.pyproject.settings[\"source\"] = [\n        {\n            \"name\": \"custom\",\n            \"url\": \"https://custom.pypi.org/simple\",\n        }\n    ]\n    project.global_config[\"pypi.custom.url\"] = \"https://extra.pypi.org/simple\"\n    project.global_config[\"pypi.custom.verify_ssl\"] = False\n    project.project_config[\"pypi.custom.username\"] = \"foo\"\n    project.project_config[\"pypi.custom.password\"] = \"bar\"\n    sources = project.sources\n    assert [source.name for source in sources] == [\"pypi\", \"custom\"]\n    custom_source = sources[1]\n    assert custom_source.url == \"https://custom.pypi.org/simple\"\n    assert custom_source.verify_ssl is False\n    assert custom_source.username == \"foo\"\n    assert custom_source.password == \"bar\"\n\n\ndef test_invoke_pdm_adding_configured_args(project, pdm, mocker):\n    project.pyproject.settings[\"options\"] = {\n        \"install\": [\"--no-self\", \"--no-editable\"],\n        \"add\": [\"--no-isolation\"],\n        \"lock\": [\"--no-cross-platform\"],\n    }\n    project.pyproject.write()\n    parser = mocker.patch(\"argparse.ArgumentParser.parse_args\")\n    pdm([\"add\", \"requests\"], obj=project)\n    parser.assert_called_with([\"add\", \"--no-isolation\", \"requests\"])\n    pdm([\"install\", \"--check\"], obj=project)\n    parser.assert_called_with([\"install\", \"--no-self\", \"--no-editable\", \"--check\"])\n    pdm([\"lock\", \"--lockfile\", \"pdm.2.lock\"], obj=project)\n    parser.assert_called_with([\"lock\", \"--no-cross-platform\", \"--lockfile\", \"pdm.2.lock\"])\n    pdm([\"-c\", \"/dev/null\", \"lock\"], obj=project)\n    parser.assert_called_with([\"-c\", \"/dev/null\", \"lock\", \"--no-cross-platform\"])\n    pdm([\"--verbose\", \"add\", \"requests\"], obj=project)\n    parser.assert_called_with([\"--verbose\", \"add\", \"--no-isolation\", \"requests\"])\n\n\n@pytest.fixture()\ndef prepare_repository(repository, project):\n    repository.add_candidate(\"foo\", \"3.0\", \">=3.8,<3.13\")\n    repository.add_candidate(\"foo\", \"2.0\", \">=3.7,<3.12\")\n    repository.add_candidate(\"foo\", \"1.0\", \">=3.7\")\n    project.environment.python_requires = PySpecSet(\">=3.9\")\n    project.add_dependencies([\"foo\"])\n\n\n@pytest.mark.usefixtures(\"prepare_repository\")\n@pytest.mark.parametrize(\"is_quiet,extra_args\", [(True, (\"-q\",)), (False, ())])\ndef test_quiet_mode(pdm, project, is_quiet, extra_args, recwarn):\n    result = pdm([\"lock\", *extra_args], obj=project)\n\n    assert result.exit_code == 0\n    assert len(recwarn) > 0\n\n    assert 'For example, \"<3.13,>=3.9\"' in str(recwarn[0].message)\n    assert 'For example, \"<3.12,>=3.9\"' in str(recwarn[1].message)\n    assert (\"to suppress these warnings\" in result.stderr) is not is_quiet\n    assert project.get_locked_repository().candidates[\"foo\"].version == \"1.0\"\n\n\n@pytest.mark.usefixtures(\"prepare_repository\")\n@pytest.mark.parametrize(\"pattern,suppressed\", [(\"foo\", True), (\"bar\", False), (\"*\", True), (\"f?o\", True)])\ndef test_ignore_package_warning(pdm, project, recwarn, pattern, suppressed):\n    project.pyproject.settings[\"ignore_package_warnings\"] = [pattern]\n    result = pdm([\"lock\"], obj=project)\n\n    assert result.exit_code == 0\n    assert (len(recwarn) == 0) is suppressed\n\n\ndef test_filter_sources_with_config(project):\n    project.pyproject.settings[\"source\"] = [\n        {\"name\": \"source1\", \"url\": \"https://source1.org/simple\", \"include_packages\": [\"foo\", \"foo-*\"]},\n        {\n            \"name\": \"source2\",\n            \"url\": \"https://source2.org/simple\",\n            \"include_packages\": [\"foo-bar\", \"bar*\"],\n            \"exclude_packages\": [\"baz-*\"],\n        },\n        {\"name\": \"pypi\", \"url\": \"https://pypi.org/simple\"},\n    ]\n    repository = project.get_repository()\n\n    def expect_sources(requirement: str, expected: list[str]) -> None:\n        sources = repository.get_filtered_sources(parse_requirement(requirement))\n        assert sorted([source.name for source in sources]) == sorted(expected)\n\n    expect_sources(\"foo\", [\"source1\"])\n    expect_sources(\"foo-baz\", [\"source1\"])\n    expect_sources(\"foo-bar\", [\"source1\", \"source2\"])\n    expect_sources(\"bar-extra\", [\"source2\"])\n    expect_sources(\"baz-extra\", [\"source1\", \"pypi\"])\n\n\n@pytest.mark.parametrize(\"use_venv\", [pytest.param(True, marks=pytest.mark.network), False])\ndef test_find_interpreters_with_PDM_IGNORE_ACTIVE_VENV(\n    pdm: PDMCallable,\n    project: Project,\n    monkeypatch: pytest.MonkeyPatch,\n    use_venv: bool,\n):\n    project._saved_python = None\n    project._python = None\n    project.project_config[\"python.use_venv\"] = use_venv\n    venv.create(venv_path := project.root / \"venv\", symlinks=True)\n    monkeypatch.setenv(\"VIRTUAL_ENV\", str(venv_path))\n    monkeypatch.setenv(\"PDM_IGNORE_ACTIVE_VENV\", \"1\")\n\n    venv_python = get_venv_python(venv_path)\n    pythons = list(project.find_interpreters())\n\n    assert pythons, \"PDM should find interpreters with PDM_IGNORE_ACTIVE_VENV\"\n    # Test requires that some interpreters are available outside the venv\n    assert any(venv_python != p.executable for p in project.find_interpreters())\n    # No need to assert, exception raised if not found\n    interpreter = project.resolve_interpreter()\n    assert interpreter.executable != venv_python\n\n    if use_venv:\n        project.project_config[\"venv.in_project\"] = True\n        pdm(\"install\", strict=True, obj=project)\n        assert project._saved_python\n        python = Path(project._saved_python)\n        assert is_path_relative_to(python, project.root)\n        assert not is_path_relative_to(python, venv_path)\n\n\n@pytest.mark.parametrize(\n    \"var,key,settings,expected\",\n    [\n        (\"PDM_VAR\", \"var\", {}, \"from-env\"),\n        (\"pdm_var\", \"var\", {}, \"from-env\"),\n        (\"PDM_NOPE\", \"var\", {\"var\": \"from-settings\"}, \"from-settings\"),\n        (\"PDM_VAR\", \"var\", {\"var\": \"from-settings\"}, \"from-env\"),\n        (\"PDM_NOPE\", \"nested.var\", {\"nested\": {\"var\": \"from-settings\"}}, \"from-settings\"),\n        (\"PDM_NOPE\", \"noop\", {}, None),\n    ],\n)\ndef test_env_or_setting(\n    project: Project,\n    monkeypatch: pytest.MonkeyPatch,\n    var: str,\n    key: str,\n    settings: dict,\n    expected: str | None,\n):\n    monkeypatch.setenv(\"PDM_VAR\", \"from-env\")\n    project.pyproject.settings.update(settings)\n\n    assert project.env_or_setting(var, key) == expected\n\n\n@pytest.mark.parametrize(\n    \"var,setting,expected\",\n    [\n        (None, None, []),\n        (\"\", None, []),\n        (\" \", None, []),\n        (None, \"\", []),\n        (None, \" \", []),\n        (None, [], []),\n        (\"var\", None, [\"var\"]),\n        (\"val1,val2\", None, [\"val1\", \"val2\"]),\n        (\"val1, val2\", None, [\"val1\", \"val2\"]),\n        (\"val1, , , val2\", None, [\"val1\", \"val2\"]),\n        (None, \"val1,val2\", [\"val1\", \"val2\"]),\n        (None, [\"val1\", \"val2\"], [\"val1\", \"val2\"]),\n        (None, [\" val1\", \"val2 \"], [\"val1\", \"val2\"]),\n        (None, [\" val1\", \"\", \"val2 \", \" \"], [\"val1\", \"val2\"]),\n        (None, [\"val1,val2\", \"val3,val4\"], [\"val1,val2\", \"val3,val4\"]),\n        (\"val1,val2\", [\"val3\", \"val4\"], [\"val1\", \"val2\"]),\n    ],\n)\ndef test_env_setting_list(\n    project: Project,\n    monkeypatch: pytest.MonkeyPatch,\n    var: str | None,\n    setting: str | list[str] | None,\n    expected: list[str],\n):\n    if var is not None:\n        monkeypatch.setenv(\"PDM_VAR\", var)\n    if setting is not None:\n        project.pyproject.settings[\"var\"] = setting\n\n    assert project.environment._setting_list(\"PDM_VAR\", \"var\") == expected\n\n\ndef test_project_best_match_max(project, mocker):\n    expected = PythonVersion(\"cpython\", 3, 10, 13)\n    mocker.patch(\n        \"pdm.project.core.get_all_installable_python_versions\",\n        return_value=get_python_versions(),\n    )\n    assert project.get_best_matching_cpython_version() == expected\n\n\ndef test_project_best_match_min(project, mocker):\n    expected = PythonVersion(\"cpython\", 3, 9, 0)\n    mocker.patch(\n        \"pdm.project.core.get_all_installable_python_versions\",\n        return_value=get_python_versions(),\n    )\n    assert project.get_best_matching_cpython_version(use_minimum=True) == expected\n\n\ndef test_default_lockfile_format(project):\n    assert project.lockfile._path.name == \"pdm.lock\"\n    project.project_config[\"lock.format\"] = \"pylock\"\n    project._lockfile = None\n    assert project.lockfile._path.name == \"pylock.toml\"\n\n    with pytest.raises(ValueError):\n        project.project_config[\"lock.format\"] = \"invalid\"\n\n\ndef test_select_lockfile_format(project, pdm, capsys):\n    pdm([\"lock\"], obj=project, strict=True)\n\n    assert project.lockfile._path.name == \"pdm.lock\"\n    project.project_config[\"lock.format\"] = \"pylock\"\n    project._lockfile = None\n    capsys.readouterr()\n    assert project.lockfile._path.name == \"pdm.lock\"\n    _, err = capsys.readouterr()\n    assert \"`lock.format` is set to pylock but pylock.toml is not found\" in err\n\n    with cd(project.root):\n        pdm([\"export\", \"-f\", \"pylock\", \"-o\", \"pylock.toml\"], strict=True)\n    project._lockfile = None\n    assert project.lockfile._path.name == \"pylock.toml\"\n"
  },
  {
    "path": "tests/test_signals.py",
    "content": "from itertools import chain\nfrom unittest import mock\n\nimport pytest\n\nfrom pdm import signals\nfrom pdm.models.candidates import Candidate\nfrom pdm.models.repositories import Package\nfrom pdm.models.requirements import Requirement\n\n\ndef test_post_init_signal(project_no_init, pdm):\n    mock_handler = mock.Mock()\n    with signals.post_init.connected_to(mock_handler):\n        result = pdm([\"init\"], input=\"\\n\\n\\n\\n\\n\\n\\n\\n\", obj=project_no_init)\n        assert result.exit_code == 0\n    mock_handler.assert_called_once_with(project_no_init, hooks=mock.ANY)\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_post_lock_and_install_signals(project, pdm):\n    pre_lock = signals.pre_lock.connect(mock.Mock(), weak=False)\n    post_lock = signals.post_lock.connect(mock.Mock(), weak=False)\n    pre_install = signals.pre_install.connect(mock.Mock(), weak=False)\n    post_install = signals.post_install.connect(mock.Mock(), weak=False)\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    signals.pre_lock.disconnect(pre_lock)\n    signals.post_lock.disconnect(post_lock)\n    signals.pre_install.disconnect(pre_install)\n    signals.post_install.disconnect(post_install)\n    for mocker in (pre_lock, post_lock, pre_install, post_install):\n        mocker.assert_called_once()\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_lock_and_install_signals_injection_with_add(project, pdm):\n    pre_lock = signals.pre_lock.connect(mock.Mock(), weak=False)\n    post_lock = signals.post_lock.connect(mock.Mock(), weak=False)\n    pre_install = signals.pre_install.connect(mock.Mock(), weak=False)\n    post_install = signals.post_install.connect(mock.Mock(), weak=False)\n    pdm([\"add\", \"requests\"], obj=project, strict=True)\n    signals.pre_lock.disconnect(pre_lock)\n    signals.post_lock.disconnect(post_lock)\n    signals.pre_install.disconnect(pre_install)\n    signals.post_install.disconnect(post_install)\n\n    assert isinstance(pre_lock.call_args.kwargs[\"requirements\"], list)\n    assert all(isinstance(e, Requirement) for e in pre_lock.call_args.kwargs[\"requirements\"])\n    assert len(pre_lock.call_args.kwargs[\"requirements\"]) == 1\n\n    assert isinstance(post_lock.call_args.kwargs[\"resolution\"], dict)\n    assert all(isinstance(e, Candidate) for e in chain.from_iterable(post_lock.call_args.kwargs[\"resolution\"].values()))\n    assert len(post_lock.call_args.kwargs[\"resolution\"]) == 5\n\n    assert isinstance(pre_install.call_args.kwargs[\"packages\"], list)\n    assert all(isinstance(e, Package) for e in pre_install.call_args.kwargs[\"packages\"])\n    assert len(pre_install.call_args.kwargs[\"packages\"]) == 5\n\n    assert isinstance(post_install.call_args.kwargs[\"packages\"], list)\n    assert all(isinstance(e, Package) for e in post_install.call_args.kwargs[\"packages\"])\n    assert len(post_install.call_args.kwargs[\"packages\"]) == 5\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_lock_and_install_signals_injection_with_install(project, pdm):\n    project.add_dependencies([\"requests\"])\n\n    pre_lock = signals.pre_lock.connect(mock.Mock(), weak=False)\n    post_lock = signals.post_lock.connect(mock.Mock(), weak=False)\n    pre_install = signals.pre_install.connect(mock.Mock(), weak=False)\n    post_install = signals.post_install.connect(mock.Mock(), weak=False)\n    pdm([\"install\"], obj=project, strict=True)\n    signals.pre_lock.disconnect(pre_lock)\n    signals.post_lock.disconnect(post_lock)\n    signals.pre_install.disconnect(pre_install)\n    signals.post_install.disconnect(post_install)\n\n    assert isinstance(pre_lock.call_args.kwargs[\"requirements\"], list)\n    assert all(isinstance(e, Requirement) for e in pre_lock.call_args.kwargs[\"requirements\"])\n    assert len(pre_lock.call_args.kwargs[\"requirements\"]) == 1\n\n    assert isinstance(post_lock.call_args.kwargs[\"resolution\"], dict)\n    assert all(isinstance(e, Candidate) for e in chain.from_iterable(post_lock.call_args.kwargs[\"resolution\"].values()))\n    assert len(post_lock.call_args.kwargs[\"resolution\"]) == 5\n\n    assert isinstance(pre_install.call_args.kwargs[\"packages\"], list)\n    assert all(isinstance(e, Package) for e in pre_install.call_args.kwargs[\"packages\"])\n    assert len(pre_install.call_args.kwargs[\"packages\"]) == 5\n\n    assert isinstance(post_install.call_args.kwargs[\"packages\"], list)\n    assert all(isinstance(e, Package) for e in post_install.call_args.kwargs[\"packages\"])\n    assert len(post_install.call_args.kwargs[\"packages\"]) == 5\n\n\n@pytest.mark.usefixtures(\"working_set\")\ndef test_lock_signals_injection_with_update(project, pdm):\n    project.add_dependencies([\"requests\"])\n\n    pre_lock = signals.pre_lock.connect(mock.Mock(), weak=False)\n    post_lock = signals.post_lock.connect(mock.Mock(), weak=False)\n    pdm([\"update\"], obj=project, strict=True)\n    signals.pre_lock.disconnect(pre_lock)\n    signals.post_lock.disconnect(post_lock)\n\n    assert isinstance(pre_lock.call_args.kwargs[\"requirements\"], list)\n    assert all(isinstance(e, Requirement) for e in pre_lock.call_args.kwargs[\"requirements\"])\n    assert len(pre_lock.call_args.kwargs[\"requirements\"]) == 1\n\n    assert isinstance(post_lock.call_args.kwargs[\"resolution\"], dict)\n    assert all(isinstance(e, Candidate) for e in chain.from_iterable(post_lock.call_args.kwargs[\"resolution\"].values()))\n    assert len(post_lock.call_args.kwargs[\"resolution\"]) == 5\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "import pathlib\nimport sys\nimport unittest.mock as mock\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\nimport pytest\nimport tomlkit\n\nfrom pdm import utils\nfrom pdm._types import RepositoryConfig\nfrom pdm.cli import utils as cli_utils\nfrom pdm.cli.filters import GroupSelection\nfrom pdm.exceptions import PdmException, PdmUsageError, PDMWarning\n\n\n@pytest.mark.parametrize(\n    \"given, dirname\",\n    [\n        ((None, None, None), \"test_dirname1\"),\n        ((\"test_suffix\", None, None), \"test_dirname2\"),\n        ((\"test_suffix\", \"test_prefix\", None), \"test_dirname3\"),\n        ((\"test_suffix\", \"test_prefix\", \"test_dir\"), \"test_dirname4\"),\n        ((None, \"test_prefix\", None), \"test_dirname5\"),\n        ((None, \"test_prefix\", \"test_dir\"), \"test_dirname6\"),\n        ((None, None, \"test_dir\"), \"test_dirname7\"),\n        ((None, \"test_prefix\", \"test_dir\"), \"test_dirname8\"),\n        ((\"test_prefix\", None, \"test_dir\"), \"test_dirname9\"),\n    ],\n)\n@mock.patch(\"pdm.utils.atexit.register\")\n@mock.patch(\"pdm.utils.os.makedirs\")\n@mock.patch(\"pdm.utils.tempfile.mkdtemp\")\ndef test_create_tracked_tempdir(mock_tempfile_mkdtemp, mock_os_makedirs, mock_atexit_register, given, dirname):\n    test_suffix, test_prefix, _ = given\n    mock_tempfile_mkdtemp.return_value = dirname\n    received_dirname = utils.create_tracked_tempdir(suffix=test_suffix, prefix=test_prefix, dir=dirname)\n    mock_tempfile_mkdtemp.assert_called_once_with(suffix=test_suffix, prefix=test_prefix, dir=dirname)\n    mock_os_makedirs.assert_called_once_with(dirname, mode=0o777, exist_ok=True)\n    mock_atexit_register.assert_called()\n    assert received_dirname == dirname\n\n\ndef test_get_trusted_hosts(mocker):\n    repository_configs = [\n        {\n            \"url\": \"https://pypi.org\",\n            \"verify_ssl\": False,\n        },\n        {\"url\": \"https://untrusted.pypi.index\", \"verify_ssl\": True},\n        {\"url\": \"https://user:password@trusted1.pypi.index\", \"verify_ssl\": False},\n        {\"url\": \"https://user:password@trusted2.pypi.index\", \"verify_ssl\": False},\n    ]\n    sources = [mocker.create_autospec(RepositoryConfig, instance=False, **config) for config in repository_configs]\n    expected = [\n        \"pypi.org\",\n        \"trusted1.pypi.index\",\n        \"trusted2.pypi.index\",\n    ]\n    received = utils.get_trusted_hosts(sources)\n    assert received == expected\n\n\n@pytest.mark.parametrize(\n    \"given, expected\",\n    [\n        (\"scheme://netloc\", \"scheme://netloc\"),\n        (\"scheme://netloc/path\", \"scheme://netloc/path\"),\n        (\"scheme://netloc/path/#\", \"scheme://netloc/path/\"),\n        (\"scheme://netloc/path#fragment\", \"scheme://netloc/path\"),\n        (\"scheme://netloc/path;parameters?query#fragment\", \"scheme://netloc/path;parameters?query\"),\n    ],\n)\ndef test_url_without_fragments(given, expected):\n    received = utils.url_without_fragments(given)\n    assert received == expected\n\n\n@pytest.mark.parametrize(\n    \"given, expected\",\n    [\n        (([\"abc\", \"def\", \"ghi\"], \"/\"), [\"abc\", \"/\", \"def\", \"/\", \"ghi\"]),\n        (([], \"/\"), []),\n        (([\"abc\"], \"/\"), [\"abc\"]),\n    ],\n)\ndef test_join_list_with(given, expected):\n    items, sep = given\n    received = utils.join_list_with(items, sep)\n    assert received == expected\n\n\nclass TestGetUserEmailFromGit:\n    @mock.patch(\"pdm.utils.shutil.which\", return_value=None)\n    def test_no_git(self, no_git_patch):\n        with no_git_patch:\n            assert utils.get_user_email_from_git() == (\"\", \"\")\n\n    @mock.patch(\n        \"pdm.utils.subprocess.check_output\",\n        side_effect=[\n            utils.subprocess.CalledProcessError(-1, [\"git\", \"config\", \"user.name\"], \"No username\"),\n            utils.subprocess.CalledProcessError(-1, [\"git\", \"config\", \"user.email\"], \"No email\"),\n        ],\n    )\n    @mock.patch(\"pdm.utils.shutil.which\", return_value=\"git\")\n    def test_no_git_username_and_email(self, git_patch, no_git_username_and_email_patch):\n        with git_patch:\n            with no_git_username_and_email_patch:\n                assert utils.get_user_email_from_git() == (\"\", \"\")\n\n    @mock.patch(\n        \"pdm.utils.subprocess.check_output\",\n        side_effect=[\n            \"username\",\n            utils.subprocess.CalledProcessError(-1, [\"git\", \"config\", \"user.email\"], \"No email\"),\n        ],\n    )\n    @mock.patch(\"pdm.utils.shutil.which\", return_value=\"git\")\n    def test_no_git_email(self, git_patch, no_git_email_patch):\n        with git_patch:\n            with no_git_email_patch:\n                assert utils.get_user_email_from_git() == (\"username\", \"\")\n\n    @mock.patch(\n        \"pdm.utils.subprocess.check_output\",\n        side_effect=[utils.subprocess.CalledProcessError(-1, [\"git\", \"config\", \"user.name\"], \"No username\"), \"email\"],\n    )\n    @mock.patch(\"pdm.utils.shutil.which\", return_value=\"git\")\n    def test_no_git_username(self, git_patch, no_git_username_patch):\n        with git_patch:\n            with no_git_username_patch:\n                assert utils.get_user_email_from_git() == (\"\", \"email\")\n\n    @mock.patch(\"pdm.utils.subprocess.check_output\", side_effect=[\"username\", \"email\"])\n    @mock.patch(\"pdm.utils.shutil.which\", return_value=\"git\")\n    def test_git_username_and_email(self, git_patch, git_username_and_email_patch):\n        with git_patch:\n            with git_username_and_email_patch:\n                assert utils.get_user_email_from_git() == (\"username\", \"email\")\n\n\n@pytest.mark.parametrize(\n    \"given,expected\",\n    [\n        (\"git@github.com/pdm-project/pdm\", \"ssh://git@github.com/pdm-project/pdm\"),\n        (\"ssh://git@github.com/pdm-project/pdm\", \"ssh://git@github.com/pdm-project/pdm\"),\n        (\"git+ssh://git@github.com/pdm-project/pdm\", \"git+ssh://git@github.com/pdm-project/pdm\"),\n        (\"https://git@github.com/pdm-project/pdm\", \"https://git@github.com/pdm-project/pdm\"),\n        (\"file:///my/local/pdm-project/pdm\", \"file:///my/local/pdm-project/pdm\"),\n    ],\n)\ndef test_add_ssh_scheme_to_git_uri(given, expected):\n    assert utils.add_ssh_scheme_to_git_uri(given) == expected\n\n\nclass TestUrlToPath:\n    def test_non_file_url(self):\n        with pytest.raises(ValueError):\n            utils.url_to_path(\"not_a_file_scheme://netloc/path\")\n\n    @pytest.mark.skipif(sys.platform.startswith(\"win\"), reason=\"Non-Windows test\")\n    def test_non_windows_non_local_file_url(self):\n        with pytest.raises(ValueError):\n            utils.url_to_path(\"file://non_local_netloc/file/url\")\n\n    @pytest.mark.skipif(sys.platform.startswith(\"win\"), reason=\"Non-Windows test\")\n    def test_non_windows_localhost_local_file_url(self):\n        assert utils.url_to_path(\"file://localhost/local/file/path\") == \"/local/file/path\"\n\n    @pytest.mark.skipif(not sys.platform.startswith(\"win\"), reason=\"Windows test\")\n    def test_windows_localhost_local_file_url(self):\n        assert utils.url_to_path(\"file://localhost/local/file/path\") == \"\\\\local\\\\file\\\\path\"\n\n\n@pytest.mark.parametrize(\n    \"given,expected\",\n    [\n        (\"test\", \"test\"),\n        (\"\", \"\"),\n        (\"${FOO}\", \"hello\"),\n        (\"$FOO\", \"$FOO\"),\n        (\"${BAR}\", \"\"),\n        (\"%FOO%\", \"%FOO%\"),\n        (\"${FOO}_${FOO}\", \"hello_hello\"),\n    ],\n)\ndef test_expand_env_vars(given, expected, monkeypatch):\n    monkeypatch.setenv(\"FOO\", \"hello\")\n    assert utils.expand_env_vars(given) == expected\n\n\n@pytest.mark.parametrize(\n    \"given,expected\",\n    [\n        (\"https://example.org/path?arg=1\", \"https://example.org/path?arg=1\"),\n        (\n            \"https://${FOO}@example.org/path?arg=1\",\n            \"https://token%3Aoidc%2F1@example.org/path?arg=1\",\n        ),\n        (\n            \"https://${FOO}:${BAR}@example.org/path?arg=1\",\n            \"https://token%3Aoidc%2F1:p%40ssword@example.org/path?arg=1\",\n        ),\n        (\n            \"https://${FOOBAR}@example.org/path?arg=1\",\n            \"https://@example.org/path?arg=1\",\n        ),\n    ],\n)\ndef test_expand_env_vars_in_auth(given, expected, monkeypatch):\n    monkeypatch.setenv(\"FOO\", \"token:oidc/1\")\n    monkeypatch.setenv(\"BAR\", \"p@ssword\")\n    assert utils.expand_env_vars_in_auth(given) == expected\n\n\n@pytest.mark.parametrize(\n    \"os_name,given,expected\",\n    [\n        (\"posix\", (\"match\", \"repl\", \"/a/b/match/c/match/d/e\"), \"/a/b/repl/c/repl/d/e\"),\n        (\"posix\", (\"old\", \"new\", \"/path/to/old/pdm\"), \"/path/to/new/pdm\"),\n        (\"posix\", (\"match\", \"repl\", \"match/a/math/b/match/c\"), \"repl/a/math/b/repl/c\"),\n        (\"posix\", (\"match\", \"repl\", \"/some/path\"), \"/some/path\"),\n        (\"posix\", (\"match\", \"repl\", \"\"), \"\"),\n        (\"nt\", (\"old\", \"new\", \"C:\\\\Path\\\\tO\\\\old\\\\pdm\"), \"C:/Path/tO/new/pdm\"),\n        (\"nt\", (\"old\", \"new\", \"C:\\\\Path\\\\tO\\\\Old\\\\pdm\"), \"C:/Path/tO/new/pdm\"),\n        (\"nt\", (\"old\", \"new\", \"C:\\\\no\\\\matching\\\\path\"), \"C:/no/matching/path\"),\n    ],\n)\ndef test_path_replace(os_name, given, expected):\n    with mock.patch(\"pdm.utils.os_name\", os_name):\n        pattern, replace_with, dest = given\n        assert utils.path_replace(pattern, replace_with, dest) == expected\n\n\n# Only testing POSIX-style paths here\n@pytest.mark.parametrize(\n    \"given,expected\",\n    [\n        ((\"/\", \"/\"), True),\n        ((\"/a\", \"/\"), True),\n        ((\"/a/b\", \"/a\"), True),\n        ((\"/a\", \"/b\"), False),\n        ((\"a\", \"b\"), False),\n        ((\"/a/b\", \"/c/d\"), False),\n        ((\"/a/b/c\", \"/a\"), True),\n        ((\"../a/b/c\", \"../a\"), True),\n    ],\n)\ndef test_is_path_relative_to(given, expected):\n    path, other = given\n    assert utils.is_path_relative_to(path, other) == expected\n\n\nclass TestGetVenvLikePrefix:\n    def test_conda_env_with_conda_meta_in_bin(self, tmp_path: Path):\n        path = tmp_path / \"conda/bin/python3\"\n        path_parent = path.parent\n        path_parent.mkdir(parents=True)\n        path.touch()\n        path_parent.joinpath(\"conda-meta\").mkdir()\n        received = utils.get_venv_like_prefix(path)\n        expected = path_parent, True\n        assert received == expected\n\n    def test_py_env_with_pyvenv_cfg(self, tmp_path: Path):\n        path = tmp_path / \"venv/bin/python3\"\n        bin_path = path.parent\n        venv_path = path.parent.parent\n        bin_path.mkdir(parents=True)\n        path.touch()\n        venv_path.joinpath(\"pyvenv.cfg\").touch()\n\n        received = utils.get_venv_like_prefix(str(path))\n        expected = venv_path, False\n        assert received == expected\n\n    def test_conda_env_with_conda_meta(self, tmp_path: Path):\n        path = tmp_path / \"conda/bin/python3\"\n        interpreter_bin_path = path.parent\n        interpreter_bin_parent_path = interpreter_bin_path.parent\n        interpreter_bin_path.mkdir(parents=True)\n        path.touch()\n        interpreter_bin_parent_path.joinpath(\"conda-meta\").mkdir()\n\n        received = utils.get_venv_like_prefix(str(path))\n        expected = interpreter_bin_parent_path, True\n        assert received == expected\n\n    def test_virtual_env(self, monkeypatch):\n        path = Path(\"/my/venv\")\n        expected = path, False\n        monkeypatch.setenv(\"VIRTUAL_ENV\", str(path))\n        received = utils.get_venv_like_prefix(path.joinpath(\"bin\", \"python3\"))\n        assert received == expected\n\n    def test_conda_virtual_env(self, monkeypatch):\n        path = Path(\"/my/conda/venv\")\n        expected = path, True\n        monkeypatch.setenv(\"CONDA_PREFIX\", str(path))\n        received = utils.get_venv_like_prefix(path.joinpath(\"bin\", \"python3\"))\n        assert received == expected\n\n    def test_no_virtual_env(self):\n        path = Path(\"/not/a/venv/bin/python3\")\n        expected = None, False\n        received = utils.get_venv_like_prefix(str(path))\n        assert received == expected\n\n\ndef compare_python_paths(path1, path2):\n    return path1.parent == path2.parent\n\n\n@pytest.mark.path\ndef test_find_python_in_path(tmp_path):\n    assert utils.find_python_in_path(sys.executable) == pathlib.Path(sys.executable).absolute()\n\n    posix_path_to_executable = pathlib.Path(sys.executable)\n    assert compare_python_paths(\n        utils.find_python_in_path(sys.prefix),\n        posix_path_to_executable,\n    )\n\n    assert not utils.find_python_in_path(tmp_path)\n\n\n@pytest.mark.parametrize(\n    \"given,expected\",\n    [\n        (\"scheme://netloc/path@rev#fragment\", \"rev\"),\n        (\"scheme://netloc/path@rev\", \"rev\"),\n        (\"scheme://netloc/path\", \"\"),\n        (\"scheme://netloc/path#fragment\", \"\"),\n    ],\n)\ndef test_get_rev_from_url(given, expected):\n    assert utils.get_rev_from_url(given) == expected\n\n\n@pytest.mark.parametrize(\n    \"given,expected\",\n    [\n        ((\"ProjectName\", False), \"ProjectName\"),\n        ((\"ProjectName\", True), \"projectname\"),\n        ((\"1Project_Name\", False), \"1Project-Name\"),\n        ((\"1Project_Name\", True), \"1project-name\"),\n        ((\"Project-Name\", False), \"Project-Name\"),\n        ((\"Project-Name\", True), \"project-name\"),\n        ((\"Project123Name\", False), \"Project123Name\"),\n        ((\"Project123name\", True), \"project123name\"),\n        ((\"123$!ProjectName\", False), \"123-ProjectName\"),\n        ((\"123$!ProjectName\", True), \"123-projectname\"),\n        ((\"123$!Project_Name\", False), \"123-Project-Name\"),\n        ((\"123$!Project_Name\", True), \"123-project-name\"),\n        ((\"$!123Project_Name4\", False), \"-123Project-Name4\"),\n        ((\"$!123Project_Name4\", True), \"-123project-name4\"),\n    ],\n)\ndef test_normalize_name(given, expected):\n    assert utils.normalize_name(*given) == expected\n\n\nclass TestIsEditable:\n    @mock.patch(\"pdm.utils.is_egg_link\", return_value=True)\n    @mock.patch(\"pdm.compat.Distribution\")\n    def test_is_egg_link(self, distribution, is_egg_link_patch):\n        with is_egg_link_patch:\n            assert utils.is_editable(distribution) is True\n\n    @mock.patch(\"pdm.utils.is_egg_link\", return_value=False)\n    @mock.patch(\"pdm.compat.Distribution\")\n    def test_not_direct_url_distribution(self, distribution, is_egg_link_patch):\n        distribution.read_text.return_value = None\n        with is_egg_link_patch:\n            assert utils.is_editable(distribution) is False\n\n    @mock.patch(\"pdm.utils.is_egg_link\", return_value=False)\n    @mock.patch(\"pdm.compat.Distribution\")\n    def test_direct_url_distribution(self, distribution, is_egg_link_patch):\n        distribution.read_text.return_value = \"\"\"{\"dir_info\": {\"editable\": true}}\"\"\"\n        with is_egg_link_patch:\n            assert utils.is_editable(distribution) is True\n\n\ndef test_merge_dictionary():\n    target = tomlkit.item(\n        {\n            \"existing_dict\": {\"foo\": \"bar\", \"hello\": \"world\"},\n            \"existing_list\": [\"hello\"],\n        }\n    )\n    input_dict = {\n        \"existing_dict\": {\"foo\": \"baz\"},\n        \"existing_list\": [\"world\"],\n        \"new_dict\": {\"name\": \"Sam\"},\n    }\n    cli_utils.merge_dictionary(target, input_dict)\n    assert target == {\n        \"existing_dict\": {\"foo\": \"baz\", \"hello\": \"world\"},\n        \"existing_list\": [\"hello\", \"world\"],\n        \"new_dict\": {\"name\": \"Sam\"},\n    }\n\n\ndef setup_dependencies(project):\n    project.pyproject.metadata.update(\n        {\n            \"dependencies\": [\"requests\"],\n            \"optional-dependencies\": {\"web\": [\"flask\"], \"auth\": [\"passlib\"]},\n        }\n    )\n    project.pyproject.dependency_groups.update({\"test\": [\"pytest\"], \"doc\": [\"mkdocs\"]})\n    project.pyproject.write()\n\n\n@pytest.mark.parametrize(\n    \"args,golden\",\n    [\n        ({\"default\": True, \"dev\": None, \"groups\": ()}, [\"default\", \"test\", \"doc\"]),\n        (\n            {\"default\": True, \"dev\": None, \"groups\": [\":all\"]},\n            [\"default\", \"web\", \"auth\", \"test\", \"doc\"],\n        ),\n        (\n            {\"default\": True, \"dev\": True, \"groups\": [\"web\"]},\n            [\"default\", \"web\", \"test\", \"doc\"],\n        ),\n        (\n            {\"default\": True, \"dev\": None, \"groups\": [\"web\"]},\n            [\"default\", \"web\", \"test\", \"doc\"],\n        ),\n        ({\"default\": True, \"dev\": None, \"groups\": [\"test\"]}, [\"default\", \"test\"]),\n        (\n            {\"default\": True, \"dev\": None, \"groups\": [\"test\", \"web\"]},\n            [\"default\", \"test\", \"web\"],\n        ),\n        ({\"default\": True, \"dev\": False, \"groups\": [\"web\"]}, [\"default\", \"web\"]),\n        ({\"default\": False, \"dev\": None, \"groups\": ()}, [\"test\", \"doc\"]),\n    ],\n)\ndef test_dependency_group_selection(project, args, golden):\n    setup_dependencies(project)\n    selection = GroupSelection(project, **args)\n    assert sorted(golden) == sorted(selection)\n\n\n@pytest.mark.parametrize(\n    \"args,golden\",\n    [\n        ({\"groups\": [\":all\"], \"excluded_groups\": [\"web\"]}, [\"default\", \"auth\", \"doc\", \"test\"]),\n        ({\"groups\": [\":all\"], \"excluded_groups\": [\"web\", \"auth\"]}, [\"default\", \"doc\", \"test\"]),\n        ({\"groups\": [\":all\"], \"excluded_groups\": [\"default\", \"test\"]}, [\"auth\", \"doc\", \"web\"]),\n    ],\n)\ndef test_exclude_optional_groups_from_all(project, args, golden):\n    setup_dependencies(project)\n    selection = GroupSelection(project, **args)\n    assert golden == list(selection)\n\n\ndef test_prod_should_not_be_with_dev(project):\n    setup_dependencies(project)\n    selection = GroupSelection(project, default=True, dev=False, groups=[\"test\"])\n    with pytest.raises(PdmUsageError):\n        list(selection)\n\n\ndef test_deprecation_warning():\n    with pytest.warns(PDMWarning) as record:\n        utils.deprecation_warning(\"Test warning\", raise_since=\"99.99\")\n    assert len(record) == 1\n    assert str(record[0].message) == \"Test warning\"\n\n    with pytest.raises(PDMWarning):\n        utils.deprecation_warning(\"Test warning\", raise_since=\"0.0\")\n\n\ndef test_comparable_version():\n    assert utils.comparable_version(\"1.2.3\") == utils.parse_version(\"1.2.3\")\n    assert utils.comparable_version(\"1.2.3a1+local1\") == utils.parse_version(\"1.2.3a1\")\n\n\n@pytest.mark.parametrize(\"name\", [\"foO\", \"f\", \"3d\", \"f3\", \"333\", \"foo.bar\", \"foo-bar\", \"foo_bar\", \"foo-_.bar\"])\ndef test_validate_project_name(name):\n    assert utils.validate_project_name(name)\n\n\n@pytest.mark.parametrize(\"name\", [\"\", \"-\", \".foo\", \"foo_\", \"-3\", \"foo$bar\", \"a.3-\"])\ndef test_invalidate_project_name(name):\n    assert not utils.validate_project_name(name)\n\n\n@pytest.mark.parametrize(\n    \"given,sanitized\",\n    [(\"foo\", \"foo\"), (\"Foo.Bar\", \"Foo.Bar\"), (\"-foo\", \"foo\"), (\"Foo%$Bar\", \"Foo-Bar\"), (\"Foo$\", \"Foo\")],\n)\ndef test_sanitize_project_name(given, sanitized):\n    assert utils.sanitize_project_name(given) == sanitized\n\n    with pytest.raises(PdmException):\n        utils.sanitize_project_name(\"@#$\")\n\n\n@pytest.mark.parametrize(\n    \"url, redacted\",\n    [\n        (\"http://user:pass@localhost:8000/path?query=1#fragment\", \"http://*****@localhost:8000/path?query=1#fragment\"),\n        (\"https://example.com\", \"https://example.com\"),\n        (\"ftp://ftp.example.com/file.txt\", \"ftp://ftp.example.com/file.txt\"),\n    ],\n)\ndef test_hide_url(url, redacted):\n    hidden = utils.hide_url(url)\n    assert hidden.secret == url\n    assert hidden.redacted == str(hidden) == redacted\n\n\ndef test_fs_supports_link_method_when_method_doesnt_exist():\n    assert utils.fs_supports_link_method(\"nonexistent-link-method\") is False\n\n\ndef test_convert_to_datetime_when_uppercase_t_is_present():\n    original_datetime = \"2025-10-30T10:26:29Z\"\n    expected_datetime = datetime.fromisoformat(\"2025-10-30T10:26:29+00:00\")\n\n    assert utils.convert_to_datetime(original_datetime) == expected_datetime\n\n\ndef test_convert_to_datetime_when_uppercase_t_is_absent():\n    original_datetime = \"2025-10-30\"\n    expected_datetime = datetime(2025, 10, 30, tzinfo=timezone.utc)\n\n    assert utils.convert_to_datetime(original_datetime) == expected_datetime\n"
  },
  {
    "path": "tox.ini",
    "content": "# https://pypi.org/project/tox-pdm/ is needed to run this tox configuration\n[tox]\nenvlist = py3{9,10,11,12,13}\npassenv = LD_PRELOAD\nisolated_build = True\n\n[testenv]\ngroups = test\ncommands = test {posargs}\n"
  },
  {
    "path": "typings/shellingham.pyi",
    "content": "def detect_shell(pid: int | None = None, max_depth: int = 10) -> tuple[str, str]: ...\n\nclass ShellDetectionFailure(OSError): ...\n"
  }
]