[
  {
    "path": ".bandit",
    "content": "[bandit]\nexclude: tests,.tox,.eggs,.venv,.git\nskips: B101\n"
  },
  {
    "path": ".codecov.yml",
    "content": "---\n\ncodecov:\n  notify:\n    manual_trigger: true  # prevent notifications until we notify Codecov\n\n  require_ci_to_pass: false\n\ncomment: false # avoid spamming reviews\n\n...\n"
  },
  {
    "path": ".coveragerc",
    "content": "[run]\nplugins = covdefaults\nomit =\n    piptools/_compat/*\n\n[report]\ninclude = piptools/*, tests/*\nfail_under = 99\n"
  },
  {
    "path": ".flake8",
    "content": "[flake8]\nmax-line-length = 100\n# E203 conflicts with PEP8; see https://github.com/psf/black#slices\nextend-ignore = E203\n\n# flake8-pytest-style\n# PT001:\npytest-fixture-no-parentheses = true\n# PT006:\npytest-parametrize-names-type = tuple\n# PT007:\npytest-parametrize-values-type = tuple\npytest-parametrize-values-row-type = tuple\n# PT023:\npytest-mark-no-parentheses = true\n\n# flake8-typing-as-t\n# TYT02:\ntyping-as-t-imported-name = _t\n"
  },
  {
    "path": ".git_archival.txt",
    "content": "node: $Format:%H$\nnode-date: $Format:%cI$\ndescribe-name: $Format:%(describe:tags=true,match=v[0-9]*[0-9])$\n"
  },
  {
    "path": ".gitattributes",
    "content": "# fill in placeholders when `git archive` is used, setuptools-scm support\n.git_archival.txt  export-subst\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "/.github/ @jazzband/pip-tools-leads\n/tox.ini @jazzband/pip-tools-leads\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\n\nname: Bug report\nabout: Create a report to help us improve\n\n---\n\n<!-- Describe the issue briefly here. -->\n\n#### Environment Versions\n\n1. OS Type\n1. Python version: `$ python -V`\n1. pip version: `$ pip --version`\n1. pip-tools version: `$ pip-compile --version`\n\n#### Steps to replicate\n\n1. ...\n2. ...\n3. ...\n\n#### Expected result\n\n...\n\n#### Actual result\n\n...\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.md",
    "content": "---\n\nname: Feature request\nabout: Suggest an idea for this project\n\n---\n\n#### What's the problem this feature will solve?\n\n<!--\nWhat are you trying to do, that you are unable to achieve with pip-tools as it\ncurrently stands?\n-->\n\n#### Describe the solution you'd like\n\n<!-- A clear and concise description of what you want to happen. -->\n\n<!--\nProvide examples of real-world use cases that this would enable and how it\nsolves the problem described above.\n-->\n\n#### Alternative Solutions\n\n<!--\nHave you tried to workaround the problem using pip-tools or other tools? Or a\ndifferent approach to solving this issue? Please elaborate here.\n-->\n\n#### Additional context\n\n<!-- Add any other context, links, etc. about the feature here. -->\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--- Describe the changes here. --->\n\n##### Contributor checklist\n\n- [ ] Included tests for the changes.\n- [ ] A change note is created in `changelog.d/` (see [`changelog.d/README.md`]\n      for instructions) or the PR text says \"no changelog needed\".\n\n[`changelog.d/README.md`]:\nhttps://github.com/jazzband/pip-tools/blob/main/changelog.d/#readme\n\n##### Maintainer checklist\n\n- [ ] If no changelog is needed, apply the `bot:chronographer:skip` label.\n- [ ] Assign the PR to an existing or new milestone for the target version\n      (following Semantic Versioning).\n"
  },
  {
    "path": ".github/actions/cache-keys/action.yml",
    "content": "---\n\nname: placeholder\ndescription: placeholder\n\noutputs:\n  cache-key-for-dep-files:\n    description: >-\n      A cache key string derived from the dependency declaration files.\n    value: ${{ steps.calc-cache-key-files.outputs.files-hash-key }}\n\nruns:\n  using: composite\n  steps:\n  - name: >-\n      Calculate dependency files' combined hash value\n      for use in the cache key\n    id: calc-cache-key-files\n    run: |\n      from os import environ\n      from pathlib import Path\n\n      FILE_APPEND_MODE = 'a'\n\n      files_derived_hash = '${{\n          hashFiles(\n            'tox.ini',\n            'pyproject.toml',\n            '.pre-commit-config.yaml',\n            'pytest.ini',\n            'dependencies/**',\n            'dependencies/*/**',\n            'setup.cfg'\n          )\n      }}'\n\n      print(f'Computed file-derived hash is {files_derived_hash}.')\n\n      with Path(environ['GITHUB_OUTPUT']).open(\n              mode=FILE_APPEND_MODE,\n      ) as outputs_file:\n          print(\n              f'files-hash-key={files_derived_hash}',\n              file=outputs_file,\n          )\n    shell: python\n\n...\n"
  },
  {
    "path": ".github/chronographer.yml",
    "content": "branch-protection-check-name: Change log entry\naction-hints:\n  check-title-prefix: \"Chronographer: \"\n  external-docs-url: https://pip-tools.rtfd.io/en/latest/contributing/#adding-change-notes-with-prs\n  inline-markdown: |\n    See [the changelog contribution docs] for news fragment authoring gotchas.\n\n    [the changelog contribution docs]: https://pip-tools.rtfd.io/en/latest/contributing/#adding-change-notes-with-prs\nenforce-name:\n  suffix: .md\n"
  },
  {
    "path": ".github/reusables/tox-dev/workflow/reusable-tox/hooks/post-tox-run/action.yml",
    "content": "---\n\ninputs:\n  calling-job-context:\n    description: A JSON with the calling job inputs\n    type: string\n  current-job-steps:\n    description: >-\n      The `$ {{ steps }}` context passed from the reusable workflow's\n      tox job encoded as a JSON string. The caller passes it this input\n      as follows:\n      `current-job-steps: $ {{ toJSON(steps) }}`.\n    type: string\n  job-dependencies-context:\n    default: >-\n      {}\n    description: >-\n      The `$ {{ needs }}` context passed from the calling workflow\n      encoded as a JSON string. The caller is expected to form this\n      input as follows:\n      `job-dependencies-context: $ {{ toJSON(needs) }}`.\n    required: false\n    type: string\n\nruns:\n  using: composite\n  steps:\n  - name: Verify that the artifacts with expected names got created\n    if: fromJSON(inputs.calling-job-context).toxenv == 'build-dists'\n    run: >\n      # Verify that the artifacts with expected names got created\n\n\n      ls -1\n      'dist/${{\n        fromJSON(\n          inputs.job-dependencies-context\n        ).pre-setup.outputs.sdist-artifact-name\n      }}'\n      'dist/${{\n        fromJSON(\n          inputs.job-dependencies-context\n        ).pre-setup.outputs.wheel-artifact-name\n      }}'\n    shell: bash\n  - name: Store the distribution packages\n    if: fromJSON(inputs.calling-job-context).toxenv == 'build-dists'\n    uses: actions/upload-artifact@v4\n    with:\n      name: >-\n        ${{\n          fromJSON(\n            inputs.job-dependencies-context\n          ).pre-setup.outputs.dists-artifact-name\n        }}\n      # NOTE: Exact expected file names are specified here\n      # NOTE: as a safety measure — if anything weird ends\n      # NOTE: up being in this dir or not all dists will be\n      # NOTE: produced, this will fail the workflow.\n      path: |\n        dist/${{\n          fromJSON(\n            inputs.job-dependencies-context\n          ).pre-setup.outputs.sdist-artifact-name\n        }}\n        dist/${{\n          fromJSON(\n            inputs.job-dependencies-context\n          ).pre-setup.outputs.wheel-artifact-name\n        }}\n      retention-days: >-\n        ${{\n            fromJSON(\n              fromJSON(\n                inputs.job-dependencies-context\n              ).pre-setup.outputs.release-requested\n            )\n            && 90\n            || 30\n        }}\n\n...\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  merge_group:\n  pull_request:\n  push:\n    branches:\n      - main\n    tags:\n      - v*\n  workflow_call:\n    inputs:\n      cpython-pip-version:\n        description: >-\n          A JSON string with pip versions\n          to test against under CPython.\n        required: false\n        type: string\n      cpython-versions:\n        description: >-\n          A JSON string with CPython versions\n          to test against.\n        required: false\n        type: string\n      release-version:\n        description: >-\n          Target PEP440-compliant version to release.\n          Any leading `v` will be stripped off.\n        required: false\n        type: string\n      release-committish:\n        default: ''\n        description: >-\n          The commit to be released to PyPI and tagged\n          in Git as `release-version`. Normally, you\n          should keep this empty.\n        type: string\n\n    outputs:\n      dists-artifact-name:\n        description: Workflow artifact name containing dists.\n        value: ${{ jobs.pre-setup.outputs.dists-artifact-name }}\n      is-upstream-repository:\n        description: >-\n          A flag representing whether the workflow runs in the upstream\n          repository or a fork.\n        value: ${{ jobs.pre-setup.outputs.is-upstream-repository }}\n      project-name:\n        description: PyPI project name.\n        value: ${{ jobs.pre-setup.outputs.project-name }}\n      project-version:\n        description: PyPI project version string.\n        value: ${{ jobs.pre-setup.outputs.dist-version }}\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}\n  cancel-in-progress: true\n\nenv:\n  FORCE_COLOR: 1 # Request colored output from CLI tools supporting it\n  MYPY_FORCE_COLOR: 1 # MyPy's color enforcement\n  PIP_DISABLE_PIP_VERSION_CHECK: 1\n  PIP_NO_PYTHON_VERSION_WARNING: 1\n  PIP_NO_WARN_SCRIPT_LOCATION: 1\n  PRE_COMMIT_COLOR: 1\n  PY_COLORS: 1 # Recognized by the `py` package, dependency of `pytest`\n  TOX_PARALLEL_NO_SPINNER: 1\n  TOX_TESTENV_PASSENV: >-\n    FORCE_COLOR\n    MYPY_FORCE_COLOR\n    NO_COLOR\n    PY_COLORS\n    PYTEST_THEME\n    PYTEST_THEME_MODE\n    PRE_COMMIT_COLOR\n  UPSTREAM_REPOSITORY_ID: >-\n    5746963\n\njobs:\n  pre-setup:\n    name: ⚙️ Pre-set global build settings\n\n    runs-on: ubuntu-latest\n\n    timeout-minutes: 2  # network is slow sometimes when fetching from Git\n\n    defaults:\n      run:\n        shell: python\n\n    outputs:\n      # NOTE: These aren't env vars because the `${{ env }}` context is\n      # NOTE: inaccessible when passing inputs to reusable workflows.\n      dists-artifact-name: python-package-distributions\n      dist-version: >-\n        ${{ steps.normalize-dist-version.outputs.dist-version }}\n      project-name: ${{ steps.metadata.outputs.project-name }}\n      release-requested: >-\n        ${{\n            steps.request-check.outputs.release-requested || false\n        }}\n      cache-key-for-dep-files: >-\n        ${{ steps.calc-cache-key-files.outputs.cache-key-for-dep-files }}\n      sdist-artifact-name: ${{ steps.artifact-name.outputs.sdist }}\n      wheel-artifact-name: ${{ steps.artifact-name.outputs.wheel }}\n      is-upstream-repository: >-\n        ${{ toJSON(env.UPSTREAM_REPOSITORY_ID == github.repository_id) }}\n\n    steps:\n    - name: Switch to using Python 3.14 by default\n      uses: actions/setup-python@v6\n      with:\n        python-version: 3.14\n    - name: >-\n        Mark the build as untagged '${{\n            github.event.repository.default_branch\n        }}' branch build\n      id: untagged-check\n      if: >-\n        github.event_name == 'push' &&\n        github.ref_type == 'branch' &&\n        github.ref_name == github.event.repository.default_branch\n      run: |\n        from os import environ\n        from pathlib import Path\n\n        FILE_APPEND_MODE = 'a'\n\n        with Path(environ['GITHUB_OUTPUT']).open(\n                mode=FILE_APPEND_MODE,\n        ) as outputs_file:\n            print('is-untagged-devel=true', file=outputs_file)\n    - name: Mark the build as \"release request\"\n      id: request-check\n      if: inputs.release-version != ''\n      run: |\n        from os import environ\n        from pathlib import Path\n\n        FILE_APPEND_MODE = 'a'\n\n        with Path(environ['GITHUB_OUTPUT']).open(\n                mode=FILE_APPEND_MODE,\n        ) as outputs_file:\n            print('release-requested=true', file=outputs_file)\n    - name: Check out src from Git\n      uses: actions/checkout@v4\n      with:\n        fetch-depth: >-\n          ${{\n            steps.request-check.outputs.release-requested == 'true'\n            && 1 || 0\n          }}\n        ref: ${{ inputs.release-committish }}\n    - name: Scan static PEP 621 core packaging metadata\n      id: metadata\n      run: |\n        from os import environ\n        from pathlib import Path\n        from tomllib import loads as parse_toml_from_string\n\n        FILE_APPEND_MODE = 'a'\n\n        pyproject_toml_txt = Path('pyproject.toml').read_text()\n        metadata = parse_toml_from_string(pyproject_toml_txt)['project']\n        project_name = metadata[\"name\"]\n\n        with Path(environ['GITHUB_OUTPUT']).open(\n                mode=FILE_APPEND_MODE,\n        ) as outputs_file:\n            print(f'project-name={project_name}', file=outputs_file)\n    - name: >-\n        Calculate dependency files' combined hash value\n        for use in the cache key\n      if: >-\n        steps.request-check.outputs.release-requested != 'true'\n      id: calc-cache-key-files\n      uses: ./.github/actions/cache-keys\n    - name: Set up pip cache\n      if: >-\n        steps.request-check.outputs.release-requested != 'true'\n      uses: re-actors/cache-python-deps@release/v1\n      with:\n        cache-key-for-dependency-files: >-\n          ${{ steps.calc-cache-key-files.outputs.cache-key-for-dep-files }}\n    - name: Drop Git tags from HEAD for non-release requests\n      if: >-\n        steps.request-check.outputs.release-requested != 'true'\n      run: >-\n        git tag --points-at HEAD\n        |\n        xargs git tag --delete\n      shell: bash -eEuxo pipefail {0}\n    - name: Set up versioning prerequisites\n      if: >-\n        steps.request-check.outputs.release-requested != 'true'\n      run: >-\n        python -m\n        pip install\n        --user\n        setuptools-scm\n      shell: bash -eEuxo pipefail {0}\n    - name: Set the current dist version from Git\n      if: steps.request-check.outputs.release-requested != 'true'\n      id: scm-version\n      run: |\n        from os import environ\n        from pathlib import Path\n\n        import setuptools_scm\n\n        FILE_APPEND_MODE = 'a'\n\n        ver = setuptools_scm.get_version(local_scheme='dirty-tag')\n        with Path(environ['GITHUB_OUTPUT']).open(\n                mode=FILE_APPEND_MODE,\n        ) as outputs_file:\n            print(f'dist-version={ver}', file=outputs_file)\n            print(\n                f'dist-version-for-filenames={ver.replace(\"+\", \"-\")}',\n                file=outputs_file,\n            )\n    - name: Normalize dist version\n      id: normalize-dist-version\n      run: |\n        from os import environ\n        from pathlib import Path\n\n        FILE_APPEND_MODE = 'a'\n\n        dist_version = \"${{\n            steps.request-check.outputs.release-requested == 'true'\n            && inputs.release-version\n            || steps.scm-version.outputs.dist-version\n        }}\".lstrip(\"v\")\n\n        with Path(environ['GITHUB_OUTPUT']).open(\n                mode=FILE_APPEND_MODE,\n        ) as outputs_file:\n            print(\n                f\"dist-version={dist_version}\",\n                file=outputs_file,\n            )\n    - name: Set the expected dist artifact names\n      id: artifact-name\n      env:\n        PROJECT_NAME: ${{ steps.metadata.outputs.project-name }}\n      run: |\n        from os import environ\n        from pathlib import Path\n\n        FILE_APPEND_MODE = 'a'\n\n        whl_file_prj_base_name = environ['PROJECT_NAME'].replace('-', '_')\n        sdist_file_prj_base_name = (\n            whl_file_prj_base_name.\n            replace('.', '_').\n            lower()\n        )\n        dist_version = \"${{ steps.normalize-dist-version.outputs.dist-version }}\"\n\n        with Path(environ['GITHUB_OUTPUT']).open(\n                mode=FILE_APPEND_MODE,\n        ) as outputs_file:\n            print(\n                f\"sdist={sdist_file_prj_base_name !s}-{dist_version}.tar.gz\",\n                file=outputs_file,\n            )\n            print(\n                f\"wheel={whl_file_prj_base_name !s}-{dist_version}-py3-none-any.whl\",\n                file=outputs_file,\n            )\n\n  build:\n    name: >-\n      📦 Build dists\n    needs:\n    - pre-setup  # transitive, for accessing settings\n\n    uses: tox-dev/workflow/.github/workflows/reusable-tox.yml@617ca35caa695c572377861016677905e58a328c  # yamllint disable-line rule:line-length\n    with:\n      cache-key-for-dependency-files: >-\n        ${{ needs.pre-setup.outputs.cache-key-for-dep-files }}\n      check-name: Build dists under 🐍3.12\n      checkout-src-git-committish: >-\n        ${{ inputs.release-committish }}\n      checkout-src-git-fetch-depth: >-\n        ${{\n          fromJSON(needs.pre-setup.outputs.release-requested)\n          && 1\n          || 0\n        }}\n      job-dependencies-context: >-  # context for hooks\n        ${{ toJSON(needs) }}\n      python-version: 3.12\n      runner-vm-os: ubuntu-latest\n      timeout-minutes: 2\n      toxenv: build-dists\n      xfail: false\n\n  linters:\n    name: Linters\n    uses: ./.github/workflows/reusable-qa.yml\n\n  test:\n    name: ${{ matrix.os }} / ${{ matrix.python-version }} / ${{ matrix.pip-version }}\n    runs-on: ${{ matrix.os }}-latest\n    timeout-minutes: 15\n    strategy:\n      fail-fast: false\n      matrix:\n        os:\n          - Ubuntu\n          - Windows\n          - macOS\n        python-version: >-\n          ${{\n            fromJSON(\n              inputs.cpython-versions\n              && inputs.cpython-versions\n              ||\n              '[\"3.9\", \"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\", \"3.14t\"]'\n            )\n          }}\n        pip-version: >-\n          ${{\n            fromJSON(\n              inputs.cpython-pip-version\n              && inputs.cpython-pip-version\n              || '[\"supported\", \"lowest\"]'\n            )\n          }}\n    env:\n      TOXENV: >-\n        pip${{ matrix.pip-version }}${{\n          !inputs.cpython-pip-version\n          && '-coverage'\n          || ''\n        }}\n    steps:\n      - uses: actions/checkout@v5\n      - name: Set up Python ${{ matrix.python-version }} from GitHub\n        id: python-install\n        if: \"!endsWith(matrix.python-version, '-dev')\"\n        uses: actions/setup-python@v6\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Set up Python ${{ matrix.python-version }} from deadsnakes\n        if: endsWith(matrix.python-version, '-dev')\n        uses: deadsnakes/action@v2.1.1\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Log python version info (${{ matrix.python-version }})\n        run: python --version --version\n      - name: Get pip cache dir\n        id: pip-cache\n        shell: bash\n        run: |\n          echo \"dir=$(pip cache dir)\" >> \"${GITHUB_OUTPUT}\"\n      - name: Pip cache\n        uses: actions/cache@v4\n        with:\n          path: ${{ steps.pip-cache.outputs.dir }}\n          key: >-\n            ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }}-${{\n            hashFiles('pyproject.toml') }}-${{ hashFiles('tox.ini') }}-${{\n            hashFiles('.pre-commit-config.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pip-\n            ${{ runner.os }}-\n      - name: Install test dependencies\n        run: python -m pip install -U tox virtualenv\n      - name: Prepare test environment\n        # NOTE: `--parallel-live` is a workaround for the regression in\n        # NOTE: the upstream tox project that made the\n        # NOTE: `TOX_PARALLEL_NO_SPINNER=1` env var auto-enable parallelism\n        # NOTE: and disable output from the tox environments.\n        #\n        # Ref: https://github.com/tox-dev/tox/issues/3193\n        run: tox -vv --notest -p auto --parallel-live\n      - name: Test pip ${{ matrix.pip-version }}\n        # NOTE: `--parallel-live` is a workaround for the regression in\n        # NOTE: the upstream tox project that made the\n        # NOTE: `TOX_PARALLEL_NO_SPINNER=1` env var auto-enable parallelism\n        # NOTE: and disable output from the tox environments.\n        #\n        # Ref: https://github.com/tox-dev/tox/issues/3193\n        run: tox --skip-pkg-install --parallel-live\n      - name: Re-run the failing tests with maximum verbosity\n        if: >-\n          !cancelled()\n          && failure()\n        run: >-  # `exit 1` makes sure that the job remains red with flaky runs\n          python -Xutf8 -Im\n          tox\n          --parallel=auto\n          --parallel-live\n          --skip-missing-interpreters=false\n          --skip-pkg-install\n          -vvvvv\n          --\n          --continue-on-collection-errors\n          --full-trace\n          --last-failed\n          ${{ !inputs.cpython-pip-version && '--no-cov' || '' }}\n          --numprocesses=0\n          --showlocals\n          --trace-config\n          -rA\n          -vvvvv\n          && exit 1\n        shell: bash\n      - name: Upload coverage to Codecov\n        if: >-\n          !cancelled()\n          && !inputs.cpython-pip-version\n        uses: codecov/codecov-action@v5\n        with:\n          files: ./coverage.xml\n          flags: >-\n            CI-GHA,\n            OS-${{ runner.os }},\n            VM-${{ matrix.os }},\n            Py-${{ steps.python-install.outputs.python-version }},\n            Pip-${{ matrix.pip-version }}\n          name: >-\n            OS-${{ runner.os }},\n            VM-${{ matrix.os }},\n            Py-${{ steps.python-install.outputs.python-version }},\n            Pip-${{ matrix.pip-version }}\n\n  pypy:\n    name: ${{ matrix.os }} / ${{ matrix.python-version }} / ${{ matrix.pip-version }}\n    runs-on: ${{ matrix.os }}-latest\n    timeout-minutes: 9\n    strategy:\n      fail-fast: false\n      matrix:\n        os:\n          - Ubuntu\n          - MacOS\n          - Windows\n        python-version:\n          - pypy-3.10\n        pip-version:\n          - supported\n    env:\n      TOXENV: pip${{ matrix.pip-version }}\n    steps:\n      - uses: actions/checkout@v5\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v6\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Get pip cache dir\n        id: pip-cache\n        shell: bash\n        run: |\n          echo \"dir=$(pip cache dir)\" >> \"${GITHUB_OUTPUT}\"\n      - name: Pip cache\n        uses: actions/cache@v4\n        with:\n          path: ${{ steps.pip-cache.outputs.dir }}\n          key: >-\n            ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }}-${{\n            hashFiles('pyproject.toml') }}-${{ hashFiles('tox.ini') }}-${{\n            hashFiles('.pre-commit-config.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pip-\n            ${{ runner.os }}-\n      - name: Install tox\n        run: pip install tox\n      - name: Prepare test environment\n        # NOTE: `--parallel-live` is a workaround for the regression in\n        # NOTE: the upstream tox project that made the\n        # NOTE: `TOX_PARALLEL_NO_SPINNER=1` env var auto-enable parallelism\n        # NOTE: and disable output from the tox environments.\n        #\n        # Ref: https://github.com/tox-dev/tox/issues/3193\n        run: tox --notest -p auto --parallel-live\n      - name: Test pip ${{ matrix.pip-version }}\n        # NOTE: `--parallel-live` is a workaround for the regression in\n        # NOTE: the upstream tox project that made the\n        # NOTE: `TOX_PARALLEL_NO_SPINNER=1` env var auto-enable parallelism\n        # NOTE: and disable output from the tox environments.\n        #\n        # Ref: https://github.com/tox-dev/tox/issues/3193\n        run: tox --skip-pkg-install --parallel-live\n      - name: Re-run the failing tests with maximum verbosity\n        if: >-\n          !cancelled()\n          && failure()\n        run: >-  # `exit 1` makes sure that the job remains red with flaky runs\n          python -Xutf8 -Im\n          tox\n          --parallel=auto\n          --parallel-live\n          --skip-missing-interpreters=false\n          --skip-pkg-install\n          -vvvvv\n          --\n          --continue-on-collection-errors\n          --full-trace\n          --last-failed\n          --numprocesses=0\n          --showlocals\n          --trace-config\n          -rA\n          -vvvvv\n          && exit 1\n        shell: bash\n\n  coverage-summary:\n    name: Coverage processing\n    if: >-\n      !cancelled()\n    runs-on: ubuntu-latest\n    timeout-minutes: 1\n    needs:\n      - test\n    steps:\n      - name: Notify Codecov that all coverage reports have been uploaded\n        if: >-\n          !cancelled()\n        uses: codecov/codecov-action@v5\n        with:\n          fail_ci_if_error: true\n          run_command: send-notifications\n\n  zizmor:\n    name: 🌈 zizmor\n    permissions:\n      security-events: write\n\n    # yamllint disable-line rule:line-length\n    uses: zizmorcore/workflow/.github/workflows/reusable-zizmor.yml@1e20adb0862e932363a4d85d68c92e5cc6fcb5d4\n\n  check: # This job does nothing and is only used for the branch protection\n    if: always()\n\n    needs:\n      - linters\n      - pypy\n      - test\n      - zizmor\n\n    runs-on: ubuntu-latest\n\n    timeout-minutes: 1\n\n    steps:\n      - name: Decide whether the needed jobs succeeded or failed\n        uses: re-actors/alls-green@afee1c1eac2a506084c274e9c02c8e0687b48d9e\n        with:\n          jobs: ${{ toJSON(needs) }}\n"
  },
  {
    "path": ".github/workflows/cron.yml",
    "content": "name: Cron\n\non:\n  schedule:\n    # Run everyday at 03:53 UTC\n    - cron: 53 3 * * *\n\njobs:\n  main:\n    name: CI\n    uses: ./.github/workflows/ci.yml\n    with:\n      cpython-versions: >-\n        [\"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\", \"3.14t\"]\n      cpython-pip-version: >-\n        [\"main\", \"latest\", \"supported\", \"lowest\"]\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "---\nname: 📦 Packaging\n\non:\n  release:\n    types:\n      - published\n\nenv:\n  FORCE_COLOR: 1 # Request colored output from CLI tools supporting it\n  MYPY_FORCE_COLOR: 1 # MyPy's color enforcement\n  PIP_DISABLE_PIP_VERSION_CHECK: 1 # Hide \"there's a newer pip\" message\n  PIP_NO_PYTHON_VERSION_WARNING: 1 # Hide \"this Python is deprecated\" message\n  PIP_NO_WARN_SCRIPT_LOCATION: 1 # Hide \"script dir is not in $PATH\" message\n  PRE_COMMIT_COLOR: always\n  PY_COLORS: 1 # Recognized by the `py` package, dependency of `pytest`\n  PYTHONIOENCODING: utf-8\n  PYTHONUTF8: 1\n  TOX_PARALLEL_NO_SPINNER: 1 # Disable tox's parallel run spinner animation\n  TOX_TESTENV_PASSENV: >- # Make tox-wrapped tools see color requests\n    FORCE_COLOR\n    MYPY_FORCE_COLOR\n    NO_COLOR\n    PIP_DISABLE_PIP_VERSION_CHECK\n    PIP_NO_PYTHON_VERSION_WARNING\n    PIP_NO_WARN_SCRIPT_LOCATION\n    PRE_COMMIT_COLOR\n    PY_COLORS\n    PYTEST_THEME\n    PYTEST_THEME_MODE\n    PYTHONIOENCODING\n    PYTHONLEGACYWINDOWSSTDIO\n    PYTHONUTF8\n\nrun-name: >-\n  ${{\n    github.event.action == 'published'\n    && format('📦 Releasing {0}...', github.ref_name)\n    || format('🌱 Smoke-testing packaging for commit {0}', github.sha)\n  }}\n  triggered by: ${{ github.event_name }} of ${{\n    github.ref\n  }} ${{\n    github.ref_type\n  }}\n  (workflow run ID: ${{\n    github.run_id\n  }}; number: ${{\n    github.run_number\n  }}; attempt: ${{\n    github.run_attempt\n  }})\n\njobs:\n  build-and-test:\n    name: >-\n      📦 ${{ github.ref_name }}\n      [mode: ${{\n        github.event.action == 'published'\n        && 'release' || 'nightly'\n      }}]\n    uses: ./.github/workflows/ci.yml\n    with:\n      release-version: ${{ github.ref_name }}\n      release-committish: ''\n\n  publish-pypi:\n    name: >-\n      📦\n      Publish v${{\n        needs.build-and-test.outputs.project-version\n      }} to PyPI\n    needs:\n      - build-and-test\n    if: >-\n      github.event.action == 'published'\n      && fromJSON(needs.build-and-test.outputs.is-upstream-repository)\n\n    runs-on: ubuntu-latest\n\n    timeout-minutes: 2 # docker+network are slow sometimes\n\n    environment:\n      name: pypi\n      url: >-\n        https://pypi.org/project/${{\n          needs.build-and-test.outputs.project-name\n        }}/${{\n          needs.build-and-test.outputs.project-version\n        }}\n\n    permissions:\n      id-token: write # PyPI Trusted Publishing (OIDC)\n\n    steps:\n      - name: Download all the dists\n        uses: actions/download-artifact@v4\n        with:\n          name: >-\n            ${{ needs.build-and-test.outputs.dists-artifact-name }}\n          path: dist/\n      - name: >-\n          📦\n          Publish v${{\n            needs.build-and-test.outputs.project-version\n          }} to PyPI\n          🔏\n        uses: pypa/gh-action-pypi-publish@release/v1\n      - name: Clean up the publish attestation leftovers\n        run: rm -fv dist/*.publish.attestation\n      - name: Upload packages to Jazzband\n        uses: pypa/gh-action-pypi-publish@release/v1\n        with:\n          user: jazzband\n          password: ${{ secrets.JAZZBAND_RELEASE_KEY }}\n          repository-url: >-\n            https://jazzband.co/projects/${{\n              needs.build-and-test.outputs.project-name\n            }}/upload\n"
  },
  {
    "path": ".github/workflows/reusable-qa.yml",
    "content": "name: QA\n\non:\n  workflow_call:\n\njobs:\n  qa:\n    name: ${{ matrix.toxenv }}\n    runs-on: ubuntu-latest\n    timeout-minutes: 2  # network is slow sometimes\n    strategy:\n      fail-fast: false\n      matrix:\n        toxenv:\n          - readme\n          - build-docs\n          - linkcheck-docs\n          - changelog-draft\n        python-version:\n          - \"3.13\"\n    env:\n      PY_COLORS: 1\n      TOXENV: ${{ matrix.toxenv }}\n      TOX_PARALLEL_NO_SPINNER: 1\n    steps:\n      - uses: actions/checkout@v5\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v6\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Get pip cache dir\n        id: pip-cache\n        run: |\n          echo \"dir=$(pip cache dir)\" >> \"${GITHUB_OUTPUT}\"\n      - name: Pip cache\n        uses: actions/cache@v4\n        with:\n          path: ${{ steps.pip-cache.outputs.dir }}\n          key: >-\n            ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }}-${{\n            hashFiles('pyproject.toml') }}-${{ hashFiles('tox.ini') }}-${{\n            hashFiles('.pre-commit-config.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pip-\n            ${{ runner.os }}-\n      - name: Prepare cache key\n        id: cache-key\n        run: echo \"sha-256=$(python -VV | sha256sum | cut -d' ' -f1)\" >> \"${GITHUB_OUTPUT}\"\n      - uses: actions/cache@v4\n        with:\n          path: ~/.cache/pre-commit\n          key: pre-commit|${{ steps.cache-key.outputs.sha-256 }}|${{ hashFiles('.pre-commit-config.yaml') }}\n      - name: Install tox\n        run: pip install tox\n      - name: Prepare test environment\n        run: tox -vv --notest -p auto --parallel-live\n      - name: Test ${{ matrix.toxenv }}\n        run: tox --skip-pkg-install\n"
  },
  {
    "path": ".gitignore",
    "content": "# Ignore cram test output\n*.t.err\n\n# Python cruft\n*.pyc\n\n# Virtualenvs\n.envrc\n.direnv\n.venv\nvenv/\n\n# Testing\n.pytest_cache/\n.tox\nhtmlcov\n\n# Build output\nbuild\ndist\n*.egg-info\n.coverage\n.coverage.*\ncoverage.xml\n.cache\n\n# IDE\n.idea\n\n# Test files\nrequirements.in\nrequirements.txt\n.eggs/\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/psf/black-pre-commit-mirror\n    rev: 26.3.1\n    hooks:\n      - id: black\n        args: [--target-version=py39]\n  - repo: https://github.com/PyCQA/isort\n    rev: 8.0.1\n    hooks:\n      - id: isort\n  - repo: https://github.com/asottile/pyupgrade\n    rev: v3.21.2\n    hooks:\n      - id: pyupgrade\n        args: [--py39-plus]\n\n  - repo: https://github.com/python-jsonschema/check-jsonschema.git\n    rev: 0.37.0\n    hooks:\n      - id: check-github-actions\n      - id: check-github-workflows\n      - id: check-jsonschema\n        alias: enforce-gha-timeouts\n        name: Check GitHub Workflows set timeout-minutes\n        args:\n          - --builtin-schema\n          - github-workflows-require-timeout\n        files: ^\\.github/workflows/[^/]+$\n        types:\n          - yaml\n      - id: check-readthedocs\n\n  - repo: https://github.com/PyCQA/flake8\n    rev: 7.3.0\n    hooks:\n      - id: flake8\n        additional_dependencies:\n          - flake8-pytest-style == 2.2.0\n          - flake8-typing-as-t == 1.1.0\n  - repo: https://github.com/pre-commit/mirrors-mypy\n    rev: v1.19.1\n    hooks:\n      - id: mypy\n        # Avoid error: Duplicate module named 'setup'\n        # https://github.com/python/mypy/issues/4008\n        # Keep exclude in sync with mypy own excludes\n        exclude: ^tests/test_data/\n        additional_dependencies:\n          - click==8.0.1\n          - pep517==0.10.0\n          - toml==0.10.2\n          - pip==20.3.4\n          - build==1.0.0\n          - pyproject_hooks==1.0.0\n          - pytest==7.4.2\n  - repo: https://github.com/PyCQA/bandit\n    rev: 1.9.4\n    hooks:\n      - id: bandit\n        args: [--ini, .bandit]\n        exclude: ^tests/\n\n  - repo: local\n    hooks:\n    - id: changelogs-md\n      name: changelog filenames\n      language: fail\n      entry: >-\n        Changelog files must be named\n        ####.(\n        highlights\n        | bugfix\n        | feature\n        | deprecation\n        | breaking\n        | doc\n        | packaging\n        | contrib\n        | misc\n        | afterword\n        )(.#)?(.md)?\n      exclude: >-\n        (?x)\n        ^\n          changelog.d/(\n            \\.gitignore\n            |\\.towncrier_template\\.md\\.jinja\n            |\\.draft_changelog_partial\\.md\n            |README\\.md\n            |(\\d+|[0-9a-f]{8}|[0-9a-f]{7}|[0-9a-f]{40}|\\+[^.]+)\\.(\n              highlights\n              |bugfix\n              |feature\n              |deprecation\n              |breaking\n              |doc\n              |packaging\n              |contrib\n              |misc\n              |afterword\n            )(\\.\\d+)?(\\.md)?\n          )\n        $\n      files: ^changelog\\.d/\n      types: []\n      types_or:\n      - file\n      - symlink\n\n  - repo: https://github.com/rhysd/actionlint.git\n    rev: v1.7.11\n    hooks:\n      - id: actionlint\n        additional_dependencies:\n          # actionlint has a shellcheck integration which extracts shell scripts in `run:` steps from GitHub Actions\n          # and checks these with shellcheck.\n          # The integration only works if shellcheck is installed.\n          - \"github.com/wasilibs/go-shellcheck/cmd/shellcheck@v0.11.1\"\n\n  - repo: https://github.com/jackdewinter/pymarkdown.git\n    rev: v0.9.36\n    hooks:\n    - id: pymarkdown\n      alias: md-changelog\n      name: PyMarkdown (published change log)\n      args:\n      # NOTE: `no-emphasis-as-heading` trips on dates under version\n      # NOTE: titles. Wrapping the dates with `<time>`, OTOH, causes\n      # NOTE: `no-inline-html`. `no-duplicate-heading` is caused by\n      # NOTE: having the same change log categories under multiple\n      # NOTE: versions. Finally, `blanks-around-headings` and\n      # NOTE: `no-multiple-blanks` are caused by our towncrier template\n      # NOTE: injecting an additional visual separator line at the end\n      # NOTE: of each release — this behavior is desired.\n      - --disable-rules=blanks-around-headings,no-duplicate-heading,no-emphasis-as-heading,no-multiple-blanks\n      - scan\n      files: ^CHANGELOG\\.md$\n    - id: pymarkdown\n      alias: md-changelog-fragments\n      name: PyMarkdown (change log fragments)\n      args:\n      # NOTE: Towncrier-processed change log fragments must not have\n      # NOTE: headings but `first-line-heading` demands the opposite.\n      - --disable-rules=first-line-heading\n      - scan\n      files: ^changelog\\.d/(.*\\..*|\\.draft_changelog_partial)\\.md$\n    - id: pymarkdown\n      alias: md-issue-pr-templates\n      name: PyMarkdown (issue and pull request templates)\n      args:\n      # NOTE: GitHub issue templates are YAML document sequences. The\n      # NOTE: first document is YAML and the second is markdown.\n      # NOTE: The beginning cannot adhere to the `first-line-heading`\n      # NOTE: requirement. The PR templates are pure Markdown but the\n      # NOTE: structure does not need a global heading since this role\n      # NOTE: is fulfilled by the PR title field.\n      - --disable-rules=first-line-heading\n      - scan\n      files: ^\\.github/(ISSUE_TEMPLATE/[^/]*|PULL_REQUEST_TEMPLATE)\\.md$\n    - id: pymarkdown\n      alias: md-generic\n      name: PyMarkdown (remaining)\n      exclude: >-\n        (?x)\n        ^\n          (\n            \\.github/(ISSUE_TEMPLATE/[^/]*|PULL_REQUEST_TEMPLATE)\\.md\n            |changelog\\.d/(\n              \\.draft_changelog_partial\\.md\n              |(\\d+|[0-9a-f]{8}|[0-9a-f]{7}|[0-9a-f]{40}|\\+[^.]+)\\.(\n                highlights\n                |bugfix\n                |feature\n                |deprecation\n                |breaking\n                |doc\n                |packaging\n                |contrib\n                |misc\n                |afterword\n              )(\\.\\d+)?(\\.md)?\n            )\n            |CHANGELOG\\.md\n          )\n        $\n\n...\n"
  },
  {
    "path": ".pre-commit-hooks.yaml",
    "content": "- id: pip-compile\n  name: pip-compile\n  description: Automatically compile requirements.\n  entry: pip-compile\n  language: python\n  files: ^requirements\\.(in|txt)$\n  pass_filenames: false\n"
  },
  {
    "path": ".pymarkdown.yml",
    "content": "---\n\n{}\n\n...\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "# https://docs.readthedocs.io/en/stable/config-file/v2.html\n\nversion: 2\n\nbuild:\n  os: ubuntu-24.04\n\n  # in order to have RTD use our tox configuration for the build, we take full\n  # control over 'build.commands'\n  # see also: https://github.com/astral-sh/uv/issues/10074#issuecomment-3128225815\n  commands:\n    # install/setup uv\n    - asdf plugin add uv\n    - asdf install uv latest\n    - asdf global uv latest\n    # use uv to get\n    # - Python 3.13\n    # - tox (with tox-uv)\n    - uv tool install tox --with tox-uv --python \"3.13\" --managed-python\n    # create the tox environment (`--notest` skips commands)\n    - uv tool run tox run -e build-docs --notest -vvvvv\n    # do the actual build step, to the RTD documented output directory\n    - uv tool run tox run -e build-docs --skip-pkg-install -q -- \"${READTHEDOCS_OUTPUT}\"/html -b dirhtml\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Change log\n\n<!-- towncrier release notes start -->\n\n## v7.5.3\n\n*2026-02-09*\n\n### Bug fixes\n\n- The option `--unsafe-package` is now normalized -- by {user}`shifqu`.\n\n  *PRs and issues:* {issue}`2150`\n\n- Fixed a bug in which `pip-compile` lost any index URL options when\n  looking up hashes -- by {user}`sirosen`.\n\n  This caused errors when a package was only available from an extra\n  index, and caused `pip-compile` to incorrectly drop index URL options\n  from output, even when they were present in the input requirements.\n\n  *PRs and issues:* {issue}`2220`, {issue}`2294`, {issue}`2305`\n\n- Fixed removal of temporary files used when reading requirements from stdin\n  -- by {user}`sirosen`.\n\n### Features\n\n- `pip-tools` is now tested against Python 3.14 and 3.14t in CI, and\n  marks them as supported in the core packaging metadata\n  -- by {user}`webknjaz`.\n\n  *PRs and issues:* {issue}`2255`\n\n- pip-tools is now compatible with pip 26.0 -- by {user}`sirosen`.\n\n  *PRs and issues:* {issue}`2319`, {issue}`2320`\n\n### Removals and backward incompatible breaking changes\n\n- Removed support for Python 3.8 -- by {user}`sirosen`.\n\n### Improved documentation\n\n- The change log management infra now allows the maintainers to add notes\n  before and after the regular categories -- by {user}`webknjaz`.\n\n  *PRs and issues:* {issue}`2287`, {issue}`2322`\n\n- Added documentation clarifying that `pip-compile` reads the existing\n  output file as a constraint source, and how to use `--upgrade` to\n  refresh dependencies -- by {user}`maliktafheem`.\n\n  *PRs and issues:* {issue}`2307`\n\n### Packaging updates and notes for downstreams\n\n- `pip-tools` is now tested against Python 3.14 and 3.14t in CI, and\n  marks them as supported in the core packaging metadata\n  -- by {user}`webknjaz`.\n\n  *PRs and issues:* {issue}`2255`\n\n### Contributor-facing changes\n\n- Consistency of the Markdown files is now being enforced by linting\n  with {pypi}`pymarkdownlnt` -- by {user}`webknjaz`.\n\n  *PRs and issues:* {issue}`2256`\n\n- The linting is now set up to perform structured GitHub Actions\n  workflows and actions checks against json schemas\n  -- by {user}`webknjaz`.\n\n  *PRs and issues:* {issue}`2273`\n\n- The CI/CD is now set up so that the distribution build job\n  is a part of the test pipeline. That pipeline is included in\n  the release workflow which sources the artifact in produces.\n  The tests must now pass for the release to be published to PyPI.\n\n  -- by {user}`webknjaz`\n\n  *PRs and issues:* {issue}`2274`\n\n- Fix `actionlint` hook usage to always include `shellcheck` integration -- by {user}`sirosen`.\n\n  *PRs and issues:* {issue}`2281`\n\n- Utilities for interacting with `pip` have started to move into the\n  :py:mod:`piptools._internal._pip_api` subpackage -- by {user}`sirosen`.\n\n  *PRs and issues:* {issue}`2285`\n\n- The change log management infra now allows the maintainers to add notes\n  before and after the regular categories -- by {user}`webknjaz`.\n\n  *PRs and issues:* {issue}`2287`, {issue}`2322`\n\n- The linting is now set up to demand that {py:mod}`typing` is always\n  imported as a module under the name of `_t` -- by {user}`webknjaz`.\n\n  This is enforced by {user}`sirosen`'s {pypi}`flake8-typing-as-t`\n  plugin for {pypi}`flake8`.\n\n  *PRs and issues:* {issue}`2289`\n\n- The {file}`tox.ini` and {file}`.github/` parts of the repository now\n  have project leads assigned as GitHub code owners -- by {user}`webknjaz`.\n\n  *PRs and issues:* {issue}`2291`\n\n- Remove a redundant 'v' prefix from the CI release workflow job name -- by {user}`anandvenugopal-tech`.\n\n  *PRs and issues:* {issue}`2300`\n\n- The `check-jsonschema` ReadTheDocs hook has been enabled, and\n  the config has been tweaked to pass -- by {user}`sirosen`.\n\n\n## v7.5.2\n\n*2025-11-11*\n\n### Bug fixes\n\n- Fixed `pip-compile` to handle relative path includes which are not subpaths of\n  the current working directory -- by {user}`sirosen`.\n\n  *PRs and issues:* {issue}`2231`, {issue}`2260`\n\n- Using `--upgrade-package` and dynamically building project metadata no\n  longer causes an {exc}`AttributeError` when pip encounters an error during the\n  build -- by {user}`Epic_Wink` and {user}`tusharsadhwani`.\n\n  *PRs and issues:* {issue}`2258`\n\n### Features\n\n- Test and declare Python 3.13 support -- by {user}`jayaddison` (for OpenCulinary).\n\n  *PRs and issues:* {issue}`2251`\n\n- pip-tools is now compatible with pip 25.3 -- by {user}`shifqu`.\n\n  *PRs and issues:* {issue}`2252`, {issue}`2253`\n\n### Packaging updates and notes for downstreams\n\n- `pip-tools` now supports installation from git archives by providing\n  `setuptools-scm` with `.git_archival.txt` data.\n\n  *PRs and issues:* {issue}`2225`\n\n### Contributor-facing changes\n\n- The [change log entry bot] has been explicitly configured to stop requiring\n  news fragments in pull requests having the [`bot:chronographer:skip` label] set\n  -- by {user}`sirosen` and {user}`webknjaz`.\n\n  It was also set up to reference our change log authoring document from the\n  GitHub Checks pages. And the reported check name is now set to `Change log entry`.\n\n  [change log entry bot]: https://github.com/sanitizers/chronographer-github-app\n  [`bot:chronographer:skip` label]: https://github.com/jazzband/pip-tools/labels/bot:chronographer:skip\n\n  *PRs and issues:* {issue}`2201`\n\n- The CI is now set up to invoke failed tests again with\n  maximum level of detail -- by {user}`webknjaz`.\n\n  The change is aimed at helping troubleshoot failures\n  that might be difficult to reproduce locally.\n\n  *PRs and issues:* {issue}`2254`\n\n- The integration with Codecov has been updated to ensure that reports\n  are uploaded to the service even on failures -- by {user}`webknjaz`.\n\n  GitHub Actions is now configured to also send an explicit notification\n  to Codecov about the completion of previously initiated uploads.\n\n  Additionally, the configuration file is now {file}`.codecov.yml`.\n\n  *PRs and issues:* {issue}`2265`\n\n- The linting suite now runs [`actionlint`] -- by {user}`webknjaz`.\n\n  This tool checks typical problems with GitHub Actions workflow\n  definitions and has a registry of widely-used GitHub Action\n  arguments that it validates.\n\n  [`actionlint`]: https://rhysd.github.io/actionlint/\n\n  *PRs and issues:* {issue}`2266`\n\n\n## v7.5.1\n\n*2025-09-26*\n\n### Bug fixes\n\n- Fixed static parsing of {file}`pyproject.toml` data when the\n  {file}`pyproject.toml` is supplied as a relative path -- by {user}`sirosen`.\n\n  *PRs and issues:* {issue}`2215`, {issue}`2221`, {issue}`2233`\n\n- The \"via\" paths in `pip-compile` output for requirements discovered from\n  `pyproject.toml` data are now written in POSIX format -- by {user}`sirosen`.\n\n  *PRs and issues:* {issue}`2221`\n\n- Fixed a bug which removed slashes from URLs in ``-r`` and ``-c`` in the output\n  of ``pip-compile`` -- by {user}`sirosen`.\n\n  *PRs and issues:* {issue}`2223`\n\n- Fixed an incompatibility with ``click >= 8.3`` which made ``pip-compile``\n  display incorrect options in the compile command in output headers\n  -- by {user}`sirosen`.\n\n  *PRs and issues:* {issue}`2235`\n\n### Features\n\n- `pip-tools` now officially supports `pip` version 25.2 -- by {user}`sirosen`.\n\n  *PRs and issues:* {issue}`2214`\n\n### Improved documentation\n\n- ReadTheDocs builds for `pip-tools` no longer include htmlzip and pdf outputs\n  -- by {user}`sirosen`.\n\n  *PRs and issues:* {issue}`2218`\n\n### Contributor-facing changes\n\n- `pip-tools` now tests on `pip` version 25.2 -- by {user}`sirosen`.\n\n  *PRs and issues:* {issue}`2214`\n\n- The changelog documentation for contributors now provides hyperlinks to the\n  source of each example change note -- by {user}`jayaddison`\n  (for OpenCulinary).\n\n  *PRs and issues:* {issue}`2217`\n\n- The CPython versions tested in nightly CI runs are now separate from\n  branch and PR CI, and don't include very old versions -- by {user}`sirosen`.\n\n  *PRs and issues:* {issue}`2226`\n\n\n## v7.5.0\n\n*2025-07-30*\n\n### Bug fixes\n\n- Fixed the ordering of format controls to preserve underlying `pip` behavior\n  -- by {user}`sethmlarson`.\n\n  *PRs and issues:* {issue}`2082`\n\n- Fixed `NoCandidateFound` exception to be compatible with `pip >= 24.1`\n  -- by {user}`chrysle`.\n\n  *PRs and issues:* {issue}`2083`\n\n- `pip-compile` now produces relative paths for editable dependencies\n  -- by {user}`macro1`.\n\n  *PRs and issues:* {issue}`2087`\n\n- Fixed crash failures due to incompatibility with `pip >= 25.1`\n  -- by {user}`gkreitz` and {user}`sirosen`.\n\n  *PRs and issues:* {issue}`2176`, {issue}`2178`\n\n### Features\n\n- `pip-compile` now treats package versions requested on the command line as\n  constraints for the underlying `pip` usage.\n  This applies to build deps in addition to normal package requirements.\n\n  -- by {user}`chrysle`\n\n  *PRs and issues:* {issue}`2106`\n\n- `pip-tools` now tests on and officially supports Python 3.12\n  -- by {user}`sirosen`.\n\n  *PRs and issues:* {issue}`2188`\n\n- Requirements file paths in `pip-compile` output are now normalized to\n  POSIX-style, even when `pip-compile` is run on Windows.\n  This provides more consistent output across various platforms.\n\n  -- by {user}`sirosen`\n\n  *PRs and issues:* {issue}`2195`\n\n- `pip-tools` now tests against and supports `pip` up to version `25.1`\n  -- by {user}`sirosen`.\n\n  *PRs and issues:* {issue}`2195`\n\n### Removals and backward incompatible breaking changes\n\n- `pip-compile` will now relativize the requirements paths which are recorded in\n  its output.\n  Paths are made relative to the working directory.\n  This provides more consistent results across `pip` versions.\n\n  -- by {user}`sirosen`\n\n  *PRs and issues:* {issue}`2131`, {issue}`2195`\n\n### Packaging updates and notes for downstreams\n\n- `pip-tools` releases are now configured via Trusted Publishing.\n  This allows for signature and attestation verification via PyPI.\n\n  -- by {user}`webknjaz`\n\n  *PRs and issues:* {issue}`2149`, {issue}`2209`, {issue}`2210`\n\n### Contributor-facing changes\n\n- `pip-tools`'s CI now runs against pinned `pip` versions, declared in `tox`\n  configuration as the \"supported\" version.\n  This does not change the support policy for `pip` versions, but declares what\n  is tested and known to work.\n\n  -- by {user}`webknjaz`\n\n  *PRs and issues:* {issue}`2142`\n\n- `pip-tools` now tests against PyPy 3.10 as its supported PyPy version\n  -- by {user}`webknjaz`.\n\n  *PRs and issues:* {issue}`2146`\n\n- `pip-tools` now uses Towncrier to manage the changelog\n  -- by {user}`sirosen` and {user}`webknjaz`,\n  with suggestions from {user}`jayaddison`.\n\n  *PRs and issues:* {issue}`2201`, {issue}`2203`\n\n- `pip-tools` now uses [`sphinx-issues`](https://github.com/sloria/sphinx-issues)\n  to link to issues, PRs, commits, and user accounts\n  -- by {user}`sirosen`.\n\n  *PRs and issues:* {issue}`2202`\n\n## v7.4.1\n\n*05 Mar 2024*\n\n### Bug Fixes\n\n- Skip constraint path check ({pr}`2038`)\n  -- by {user}`honnix`.\n- Fix collecting deps for all extras in multiple input packages ({pr}`1981`)\n  -- by {user}`dragly`.\n\n## v7.4.0\n\n*16 Feb 2024*\n\n### Features\n\n- Allow force-enabling or force-disabling colorized output ({pr}`2041`)\n  -- by {user}`aneeshusa`.\n- Add support for command-specific configuration sections ({pr}`1966`)\n  -- by {user}`chrysle`.\n- Add options for including build dependencies in compiled output ({pr}`1681`)\n  -- by {user}`apljungquist`.\n\n### Bug Fixes\n\n- Fix for `src-files` not being used when specified in a config file ({pr}`2015`)\n  -- by {user}`csalerno-asml`.\n- Fix ignorance of inverted CLI options in config for `pip-sync` ({pr}`1989`)\n  -- by {user}`chrysle`.\n- Filter out origin ireqs for extra requirements before writing output\n  annotations ({pr}`2011`) -- by {user}`chrysle`.\n- Make BacktrackingResolver ignore extras when dropping existing constraints ({pr}`1984`)\n  -- by {user}`chludwig-haufe`.\n- Display `pyproject.toml`'s metatada parsing errors in verbose mode ({pr}`1979`)\n  -- by {user}`szobov`.\n\n### Other Changes\n\n- Add mention of pip-compile-multi in Other useful tools README section ({pr}`1986`)\n  -- by {user}`peterdemin`.\n\n## v7.3.0\n\n*09 Aug 2023*\n\n### Features\n\n- Add `--no-strip-extras` and warn about strip extras by default ({pr}`1954`)\n  -- by {user}`ryanhiebert`.\n\n### Bug Fixes\n\n- Fix revealed default config in header if requirements in subfolder ({pr}`1904`)\n  -- by {user}`atugushev`.\n- Direct references show extra requirements in .txt files ({pr}`1582`)\n  -- by {user}`FlorentJeannot`.\n\n### Other Changes\n\n- Document how to run under `pipx run` ({pr}`1951`)\n  -- by {user}`brettcannon`.\n- Document that the backtracking resolver is the current default ({pr}`1948`)\n  -- by {user}`jeffwidman`.\n\n## v7.2.0\n\n*02 Aug 2023*\n\n### Features\n\n- Add `-c/--constraint` option to `pip-compile` ({pr}`1936`)\n  -- by {user}`atugushev`.\n\n### Bug Fixes\n\n- Allow options in config from both `pip-compile` and `pip-sync` ({pr}`1933`)\n  -- by {user}`atugushev`.\n- Fix rejection of negating CLI boolean flags in config ({pr}`1913`)\n  -- by {user}`chrysle`.\n\n### Other Changes\n\n- Add Command Line Reference section to docs ({pr}`1934`)\n  -- by {user}`atugushev`.\n\n## v7.1.0\n\n*18 Jul 2023*\n\n### Features\n\n- Validate parsed config against CLI options ({pr}`1910`)\n  -- by {user}`atugushev`.\n\n### Bug Fixes\n\n- Fix a bug where `pip-sync` would unexpectedly uninstall some packages ({pr}`1919`)\n  -- by {user}`atugushev`.\n\n## v7.0.0\n\n*14 Jul 2023*\n\n### Backwards Incompatible Changes\n\n- Default to `--resolver=backtracking` ({pr}`1897`)\n  -- by {user}`atugushev`.\n- Drop support for Python 3.7 ({pr}`1879`)\n  -- by {user}`chrysle`.\n\n### Features\n\n- Add support for `pip==23.2` where refactored out `DEV_PKGS` ({pr}`1906`)\n  -- by {user}`atugushev`.\n- Add `--no-config` option ({pr}`1896`)\n  -- by {user}`atugushev`.\n\n### Bug Fixes\n\n- Sync direct references with hashes ({pr}`1885`)\n  -- by {user}`siddharthab`.\n- Fix missing `via`s when more than two input files are used ({pr}`1890`)\n  -- by {user}`lpulley`.\n\n## v6.14.0\n\n*28 Jun 2023*\n\n### Features\n\n- Support config defaults using `.pip-tools.toml` or `pyproject.toml` ({pr}`1863`)\n  -- by {user}`j00bar`.\n- Log a warning if the user specifies `-P` and the output file is present but\n  empty ({pr}`1822`) -- by {user}`davidmreed`.\n- Improve warning for `pip-compile` if no `--allow-unsafe` was passed ({pr}`1867`)\n  -- by {user}`chrysle`.\n\n### Other Changes\n\n- Correct in README `pre-commit` hook to run off `requirements.in` ({pr}`1847`)\n  -- by {user}`atugushev`.\n- Add pyprojects.toml example for using setuptools ({pr}`1851`)\n  -- by {user}`shatakshiiii`.\n\n## v6.13.0\n\n*07 Apr 2023*\n\n### Features\n\n- Add support for self-referential extras ({pr}`1791`)\n  -- by {user}`q0w`.\n- Add support for `pip==23.1` where removed `FormatControl` in `WheelCache` ({pr}`1834`)\n  -- by {user}`atugushev`.\n- Add support for `pip==23.1` where refactored requirement options ({pr}`1832`)\n  -- by {user}`atugushev`.\n- Add support for `pip==23.1` where deprecated `--install-option` has been\n  removed ({pr}`1828`) -- by {user}`atugushev`.\n\n### Bug Fixes\n\n- Pass `--cache-dir` to `--pip-args` for backtracking resolver ({pr}`1827`)\n  -- by {user}`q0w`.\n\n### Other Changes\n\n- Update examples in README ({pr}`1835`)\n  -- by {user}`lucaswerkmeister`.\n\n## v6.12.3\n\n*01 Mar 2023*\n\n### Bug Fixes\n\n- Remove extras from user-supplied constraints in backtracking resolver ({pr}`1808`)\n  -- by {user}`thomdixon`.\n- Fix for sync error when the ireqs being merged have no names ({pr}`1802`)\n  -- by {user}`richafrank`.\n\n## v6.12.2\n\n*25 Dec 2022*\n\n### Bug Fixes\n\n- Raise error if input and output filenames are matched ({pr}`1787`)\n  -- by {user}`atugushev`.\n- Add `pyproject.toml` as default input file format ({pr}`1780`)\n  -- by {user}`berislavlopac`.\n- Fix a regression with unsafe packages for `--allow-unsafe` ({pr}`1788`)\n  -- by {user}`q0w`.\n\n## v6.12.1\n\n*16 Dec 2022*\n\n### Bug Fixes\n\n- Set explicitly packages for setuptools ({pr}`1782`)\n  -- by {user}`q0w`.\n\n## v6.12.0\n\n*13 Dec 2022*\n\n### Features\n\n- Add `--no-index` flag to `pip-compile` ({pr}`1745`)\n  -- by {user}`atugushev`.\n\n### Bug Fixes\n\n- Treat `--upgrade-packages` PKGSPECs as constraints (not just minimums),\n  consistently ({pr}`1578`) -- by {user}`AndydeCleyre`.\n- Filter out the user provided unsafe packages ({pr}`1766`)\n  -- by {user}`q0w`.\n- Adopt PEP-621 for packaging ({pr}`1763`)\n  -- by {user}`ssbarnea`.\n\n## v6.11.0\n\n*30 Nov 2022*\n\n### Features\n\n- Add `pyproject.toml` file ({pr}`1643`)\n  -- by {user}`otherJL0`.\n- Support build isolation using `setuptools/pyproject.toml` requirement files ({pr}`1727`)\n  -- by {user}`atugushev`.\n\n### Bug Fixes\n\n- Improve punctuation/grammar with `pip-compile` header ({pr}`1547`)\n  -- by {user}`blueyed`.\n- Generate hashes for all available candidates ({pr}`1723`)\n  -- by {user}`neykov`.\n\n### Other Changes\n\n- Bump click minimum version to `>= 8` ({pr}`1733`)\n  -- by {user}`atugushev`.\n- Bump pip minimum version to `>= 22.2` ({pr}`1729`)\n  -- by {user}`atugushev`.\n\n## v6.10.0\n\n*13 Nov 2022*\n\n### Features\n\n- Deprecate `pip-compile --resolver=legacy` ({pr}`1724`)\n  -- by {user}`atugushev`.\n- Prompt user to use the backtracking resolver on errors ({pr}`1719`)\n  -- by {user}`maxfenv`.\n- Add support for Python 3.11 final ({pr}`1708`)\n  -- by {user}`hugovk`.\n- Add `--newline=[LF|CRLF|native|preserve]` option to `pip-compile` ({pr}`1652`)\n  -- by {user}`AndydeCleyre`.\n\n### Bug Fixes\n\n- Fix inconsistent handling of constraints comments with backtracking resolver ({pr}`1713`)\n  -- by {user}`mkniewallner`.\n- Fix some encoding warnings in Python 3.10 (PEP 597) ({pr}`1614`)\n  -- by {user}`GalaxySnail`.\n\n### Other Changes\n\n- Update pip-tools version in the README's pre-commit examples ({pr}`1701`)\n  -- by {user}`Kludex`.\n- Document use of the backtracking resolver ({pr}`1718`)\n  -- by {user}`maxfenv`.\n- Use HTTPS in a readme link ({pr}`1716`)\n  -- by {user}`Arhell`.\n\n## v6.9.0\n\n*05 Oct 2022*\n\n### Features\n\n- Add `--all-extras` flag to `pip-compile` ({pr}`1630`)\n  -- by {user}`apljungquist`.\n- Support Exclude Package with custom unsafe packages ({pr}`1509`)\n  -- by {user}`hmc-cs-mdrissi`.\n\n### Bug Fixes\n\n- Fix compile cached vcs packages ({pr}`1649`)\n  -- by {user}`atugushev`.\n- Include `py.typed` in wheel file ({pr}`1648`)\n  -- by {user}`FlorentJeannot`.\n\n### Other Changes\n\n- Add pyproject.toml & modern packaging to introduction ({pr}`1668`)\n  -- by {user}`hynek`.\n\n## v6.8.0\n\n*30 Jun 2022*\n\n### Features\n\n- Add support for pip's 2020 dependency resolver. Use\n  `pip-compile --resolver backtracking` to enable new resolver ({pr}`1539`)\n  -- by {user}`atugushev`.\n\n## v6.7.0\n\n*27 Jun 2022*\n\n### Features\n\n- Support for the `importlib.metadata` metadata implementation ({pr}`1632`)\n  -- by {user}`richafrank`.\n\n### Bug Fixes\n\n- Instantiate a new accumulator `InstallRequirement` for `combine_install_requirements`\n  output ({pr}`1519`)\n  -- by {user}`richafrank`.\n\n### Other Changes\n\n- Replace direct usage of the `pep517` module with the `build` module, for loading\n  project metadata ({pr}`1629`)\n  -- by {user}`AndydeCleyre`.\n\n## v6.6.2\n\n*23 May 2022*\n\n### Bug Fixes\n\n- Update `PyPIRepository::resolve_reqs()` for pip>=22.1.1 ({pr}`1624`)\n  -- by {user}`m000`.\n\n## v6.6.1\n\n*13 May 2022*\n\n### Bug Fixes\n\n- Fix support for pip>=22.1 ({pr}`1618`)\n  -- by {user}`wizpig64`.\n\n## v6.6.0\n\n*06 Apr 2022*\n\n### Features\n\n- Add support for pip>=22.1 ({pr}`1607`)\n  -- by {user}`atugushev`.\n\n### Bug Fixes\n\n- Ensure `pip-compile --dry-run --quiet` still shows what would be done, while omitting\n  the dry run message ({pr}`1592`)\n  -- by {user}`AndydeCleyre`.\n- Fix `--generate-hashes` when hashes are computed from files ({pr}`1540`)\n  -- by {user}`RazerM`.\n\n## v6.5.1\n\n*08 Feb 2022*\n\n### Bug Fixes\n\n- Ensure canonicalized requirement names are used as keys, to prevent unnecessary\n  reinstallations during sync ({pr}`1572`)\n  -- by {user}`AndydeCleyre`.\n\n## v6.5.0\n\n*04 Feb 2022*\n\n### Features\n\n- Add support for pip>=22.0, drop support for Python 3.6 ({pr}`1567`)\n  -- by {user}`di`.\n- Test on Python 3.11 ({pr}`1527`)\n  -- by {user}`hugovk`.\n\n### Other Changes\n\n- Minor doc edits ({pr}`1445`)\n  -- by {user}`ssiano`.\n\n## v6.4.0\n\n*12 Oct 2021*\n\n### Features\n\n- Add support for `pip>=21.3` ({pr}`1501`)\n  -- by {user}`atugushev`.\n- Add support for Python 3.10 ({pr}`1497`)\n  -- by {user}`joshuadavidthomas`.\n\n### Other Changes\n\n- Bump pip minimum version to `>= 21.2` ({pr}`1500`)\n  -- by {user}`atugushev`.\n\n## v6.3.1\n\n*08 Oct 2021*\n\n### Bug Fixes\n\n- Ensure `pip-tools` unions dependencies of multiple declarations of a package with\n  different extras ({pr}`1486`)\n  -- by {user}`richafrank`.\n- Allow comma-separated arguments for `--extra` ({pr}`1493`)\n  -- by {user}`AndydeCleyre`.\n- Improve clarity of help text for options supporting multiple ({pr}`1492`)\n  -- by {user}`AndydeCleyre`.\n\n## v6.3.0\n\n*21 Sep 2021*\n\n### Features\n\n- Enable single-line annotations with `pip-compile --annotation-style=line` ({pr}`1477`)\n  -- by {user}`AndydeCleyre`.\n- Generate PEP 440 direct reference whenever possible ({pr}`1455`)\n  -- by {user}`FlorentJeannot`.\n- PEP 440 Direct Reference support ({pr}`1392`)\n  -- by {user}`FlorentJeannot`.\n\n### Bug Fixes\n\n- Change log level of hash message ({pr}`1460`)\n  -- by {user}`plannigan`.\n- Allow passing `--no-upgrade` option ({pr}`1438`)\n  -- by {user}`ssbarnea`.\n\n## v6.2.0\n\n*22 Jun 2021*\n\n### Features\n\n- Add `--emit-options/--no-emit-options` flags to `pip-compile` ({pr}`1123`)\n  -- by {user}`atugushev`.\n- Add `--python-executable` option for `pip-sync` ({pr}`1333`)\n  -- by {user}`MaratFM`.\n- Log which python version was used during compile ({pr}`828`)\n  -- by {user}`graingert`.\n\n### Bug Fixes\n\n- Fix `pip-compile` package ordering ({pr}`1419`)\n  -- by {user}`adamsol`.\n- Add `--strip-extras` option to `pip-compile` for producing constraint compatible\n  output ({pr}`1404`)\n  -- by {user}`ssbarnea`.\n- Fix `click` v7 `version_option` compatibility ({pr}`1410`)\n  -- by {user}`FuegoFro`.\n- Pass `package_name` explicitly in `click.version_option` decorators for compatibility\n  with `click>=8.0` ({pr}`1400`)\n  -- by {user}`nicoa`.\n\n### Other Changes\n\n- Document updating requirements with `pre-commit` hooks ({pr}`1387`)\n  -- by {user}`microcat49`.\n- Add `setuptools` and `wheel` dependencies to the `setup.cfg` ({pr}`889`)\n  -- by {user}`jayvdb`.\n- Improve instructions for new contributors ({pr}`1394`)\n  -- by {user}`FlorentJeannot`.\n- Better explain role of existing `requirements.txt` ({pr}`1369`)\n  -- by {user}`mikepqr`.\n\n## v6.1.0\n\n*14 Apr 2021*\n\n### Features\n\n- Add support for `pyproject.toml` or `setup.cfg` as input dependency file\n  {pep}`517` for `pip-compile` ({pr}`1356`)\n  -- by {user}`orsinium`.\n- Add `pip-compile --extra` option to specify `extras_require` dependencies\n  ({pr}`1363`) -- by {user}`orsinium`.\n\n### Bug Fixes\n\n- Restore ability to set compile cache with env var `PIP_TOOLS_CACHE_DIR` ({pr}`1368`)\n  -- by {user}`AndydeCleyre`.\n\n## v6.0.1\n\n*15 Mar 2021*\n\n### Bug Fixes\n\n- Fixed a bug with undeclared dependency on `importlib-metadata` at Python 3.6 ({pr}`1353`)\n  -- by {user}`atugushev`.\n\n### Dependencies\n\n- Add `pep517` dependency ({pr}`1353`)\n  -- by {user}`atugushev`.\n\n## v6.0.0\n\n*12 Mar 2021*\n\n### Backwards Incompatible Changes\n\n- Remove support for EOL Python 3.5 and 2.7 ({pr}`1243`)\n  -- by {user}`jdufresne`.\n- Remove deprecated `--index/--no-index` option from `pip-compile` ({pr}`1234`)\n  -- by {user}`jdufresne`.\n\n### Features\n\n- Use `pep517` to parse dependencies metadata from `setup.py` ({pr}`1311`)\n  -- by {user}`astrojuanlu`.\n\n### Bug Fixes\n\n- Fix a bug where `pip-compile` with `setup.py` would not include dependencies with\n  environment markers ({pr}`1311`)\n  -- by {user}`astrojuanlu`.\n- Prefer `===` over `==` when generating `requirements.txt` if a dependency was pinned\n  with `===` ({pr}`1323`)\n  -- by {user}`IceTDrinker`.\n- Fix a bug where `pip-compile` with `setup.py` in nested folder would generate\n  `setup.txt` output file ({pr}`1324`)\n  -- by {user}`peymanslh`.\n- Write out default index when it is provided as `--extra-index-url` ({pr}`1325`)\n  -- by {user}`fahrradflucht`.\n\n### Dependencies\n\n- Bump `pip` minimum version to `>= 20.3` ({pr}`1340`)\n  -- by {user}`atugushev`.\n\n## v5.5.0\n\n*31 Dec 2020*\n\n### Features\n\n- Add Python 3.9 support ({pr}`1222`)\n  -- by {user}`jdufresne`.\n- Improve formatting of long \"via\" annotations ({pr}`1237`)\n  -- by {user}`jdufresne`.\n- Add `--verbose` and `--quiet` options to `pip-sync` ({pr}`1241`)\n  -- by {user}`jdufresne`.\n- Add `--no-allow-unsafe` option to `pip-compile` ({pr}`1265`)\n  -- by {user}`jdufresne`.\n\n### Bug Fixes\n\n- Restore `PIP_EXISTS_ACTION` environment variable to its previous state when resolve\n  dependencies in `pip-compile` ({pr}`1255`)\n  -- by {user}`jdufresne`.\n\n### Dependencies\n\n- Remove `six` dependency in favor `pip`'s vendored `six` ({pr}`1240`)\n  -- by {user}`jdufresne`.\n\n### Improved Documentation\n\n- Add `pip-requirements.el` (for Emacs) to useful tools to `README` ({pr}`1244`)\n  -- by {user}`jdufresne`.\n- Add supported Python versions to `README` ({pr}`1246`)\n  -- by {user}`jdufresne`.\n\n## v5.4.0\n\n*21 Nov 2020*\n\n### Features\n\n- Add `pip>=20.3` support ({pr}`1216`)\n  -- by {user}`atugushev` and {user}`AndydeCleyre`.\n- Exclude `--no-reuse-hashes` option from «command to run» header ({pr}`1197`)\n  -- by {user}`graingert`.\n\n### Dependencies\n\n- Bump `pip` minimum version to `>= 20.1` ({pr}`1191`)\n  -- by {user}`atugushev` and {user}`AndydeCleyre`.\n\n## v5.3.1\n\n*31 Jul 2020*\n\n### Bug Fixes\n\n- Fix `pip-20.2` compatibility issue that caused `pip-tools` to sometime fail to\n  stabilize in a constant number of rounds ({pr}`1194`)\n  -- by {user}`vphilippon`.\n\n## v5.3.0\n\n*26 Jul 2020*\n\n### Features\n\n- Add `-h` alias for `--help` option to `pip-sync` and `pip-compile` ({pr}`1163`)\n  -- by {user}`jan25`.\n- Add `pip>=20.2` support ({pr}`1168`)\n  -- by {user}`atugushev`.\n- `pip-sync` now exists with code `1` on `--dry-run` ({pr}`1172`)\n  -- by {user}`francisbrito`.\n- `pip-compile` now doesn't resolve constraints from `-c constraints.txt`that\n  are not (yet) requirements ({pr}`1175`) -- by {user}`clslgrnc`.\n- Add `--reuse-hashes/--no-reuse-hashes` options to `pip-compile` ({pr}`1177`)\n  -- by {user}`graingert`.\n\n## v5.2.1\n\n*09 Jun 2020*\n\n### Bug Fixes\n\n- Fix a bug where `pip-compile` would lose some dependencies on update a\n  `requirements.txt` ({pr}`1159`)\n  -- by {user}`richafrank`.\n\n## v5.2.0\n\n*27 May 2020*\n\n### Features\n\n- Show basename of URLs when `pip-compile` generates hashes in a verbose mode ({pr}`1113`)\n  -- by {user}`atugushev`.\n- Add `--emit-index-url/--no-emit-index-url` options to `pip-compile` ({pr}`1130`)\n  -- by {user}`atugushev`.\n\n### Bug Fixes\n\n- Fix a bug where `pip-compile` would ignore some of package versions when\n  `PIP_PREFER_BINARY` is set on ({pr}`1119`)\n  -- by {user}`atugushev`.\n- Fix leaked URLs with credentials in the debug output of `pip-compile` ({pr}`1146`)\n  -- by {user}`atugushev`.\n- Fix a bug where URL requirements would have name collisions ({pr}`1149`)\n  -- by {user}`geokala`.\n\n### Deprecations\n\n- Deprecate `--index/--no-index` in favor of `--emit-index-url/--no-emit-index-url`\n  options in `pip-compile` ({pr}`1130`)\n  -- by {user}`atugushev`.\n\n### Other Changes\n\n- Switch to `setuptools` declarative syntax through `setup.cfg` ({pr}`1141`)\n  -- by {user}`jdufresne`.\n\n## v5.1.2\n\n*05 May 2020*\n\n### Bug Fixes\n\n- Fix grouping of editables and non-editables requirements ({pr}`1132`)\n  -- by {user}`richafrank`.\n\n## v5.1.1\n\n*01 May 2020*\n\n### Bug Fixes\n\n- Fix a bug where `pip-compile` would generate hashes for `*.egg` files ({pr}`1122`)\n  -- by {user}`atugushev`.\n\n## v5.1.0\n\n*27 Apr 2020*\n\n### Features\n\n- Show progress bar when downloading packages in `pip-compile` verbose mode ({pr}`949`)\n  -- by {user}`atugushev`.\n- `pip-compile` now gets hashes from `PyPI` JSON API (if available) which significantly\n  increases the speed of hashes generation ({pr}`1109`)\n  -- by {user}`atugushev`.\n\n## v5.0.0\n\n*16 Apr 2020*\n\n### Backwards Incompatible Changes\n\n- `pip-tools` now requires `pip>=20.0` (previously `8.1.x` - `20.0.x`). Windows users,\n  make sure to use `python -m pip install pip-tools` to avoid issues with `pip`\n  self-update from now on ({pr}`1055`)\n  -- by {user}`atugushev`.\n- `--build-isolation` option now set on by default for `pip-compile` ({pr}`1060`)\n  -- by {user}`hramezani`.\n\n### Features\n\n- Exclude requirements with non-matching markers from `pip-sync` ({pr}`927`)\n  -- by {user}`AndydeCleyre`.\n- Add `pre-commit` hook for `pip-compile` ({pr}`976`)\n  -- by {user}`atugushev`.\n- `pip-compile` and `pip-sync` now pass anything provided to the new\n  `--pip-args` option on to `pip` ({pr}`1080`) -- by {user}`AndydeCleyre`.\n- `pip-compile` output headers are now more accurate when `--` is used to escape\n  filenames ({pr}`1080`)\n  -- by {user}`AndydeCleyre`.\n- Add `pip>=20.1` support ({pr}`1088`)\n  -- by {user}`atugushev`.\n\n### Bug Fixes\n\n- Fix a bug where editables that are both direct requirements and constraints wouldn't\n  appear in `pip-compile` output ({pr}`1093`)\n  -- by {user}`richafrank`.\n- `pip-compile` now sorts format controls (`--no-binary/--only-binary`) to ensure\n  consistent results ({pr}`1098`)\n  -- by {user}`richafrank`.\n\n### Improved Documentation\n\n- Add cross-environment usage documentation to `README` ({pr}`651`)\n  -- by {user}`vphilippon`.\n- Add versions compatibility table to `README` ({pr}`1106`)\n  -- by {user}`atugushev`.\n\n## v4.5.1\n\n*26 Feb 2020*\n\n### Bug Fixes\n\n- Strip line number annotations such as \"(line XX)\" from file requirements, to prevent\n  diff noise when modifying input requirement files ({pr}`1075`)\n  -- by {user}`adamchainz`.\n\n### Improved Documentation\n\n- Updated `README` example outputs for primary requirement annotations ({pr}`1072`)\n  -- by {user}`richafrank`.\n\n## v4.5.0\n\n*20 Feb 2020*\n\n### Features\n\n- Primary requirements and VCS dependencies are now get annotated with any\n  source `.in` files and reverse dependencies ({pr}`1058`)\n  -- by {user}`AndydeCleyre`.\n\n### Bug Fixes\n\n- Always use normalized path for cache directory as it is required in newer\n  versions of `pip` ({pr}`1062`) -- by {user}`kammala`.\n\n### Improved Documentation\n\n- Replace outdated link in the `README` with rationale for pinning ({pr}`1053`)\n  -- by {user}`m-aciek`.\n\n## v4.4.1\n\n*31 Jan 2020*\n\n### Bug Fixes\n\n- Fix a bug where `pip-compile` would keep outdated options from\n  `requirements.txt` ({pr}`1029`) -- by {user}`atugushev`.\n- Fix the `No handlers could be found for logger \"pip.*\"` error by configuring the\n  builtin logging module ({pr}`1035`)\n  -- by {user}`vphilippon`.\n- Fix a bug where dependencies of relevant constraints may be missing from\n  output file ({pr}`1037`) -- by {user}`jeevb`.\n- Upgrade the minimal version of `click` from `6.0` to `7.0` version in\n  `setup.py` ({pr}`1039`) -- by {user}`hramezani`.\n- Ensure that depcache considers the python implementation such that (for example)\n  `cpython3.6` does not poison the results of `pypy3.6` ({pr}`1050`)\n  -- by {user}`asottile`.\n\n### Improved Documentation\n\n- Make the `README` more imperative about installing into a project's virtual\n  environment to avoid confusion ({pr}`1023`)\n  -- by {user}`tekumara`.\n- Add a note to the `README` about how to install requirements on different\n  stages to\n  [Workflow for layered requirements](https://pip-tools.rtfd.io/en/latest/#workflow-for-layered-requirements)\n  section ({pr}`1044`)\n  -- by {user}`hramezani`.\n\n## v4.4.0\n\n*21 Jan 2020*\n\n### Features\n\n- Add `--cache-dir` option to `pip-compile` ({pr}`1022`)\n  -- by {user}`richafrank`.\n- Add `pip>=20.0` support ({pr}`1024`)\n  -- by {user}`atugushev`.\n\n### Bug Fixes\n\n- Fix a bug where `pip-compile --upgrade-package` would upgrade those passed packages\n  not already required according to the `*.in` and `*.txt` files ({pr}`1031`)\n  -- by {user}`AndydeCleyre`.\n\n## v4.3.0\n\n*25 Nov 2019*\n\n### Features\n\n- Add Python 3.8 support ({pr}`956`)\n  -- by {user}`hramezani`.\n- Unpin commented out unsafe packages in `requirements.txt` ({pr}`975`)\n  -- by {user}`atugushev`.\n\n### Bug Fixes\n\n- Fix `pip-compile` doesn't copy `--trusted-host` from `requirements.in` to\n  `requirements.txt` ({pr}`964`)\n  -- by {user}`atugushev`.\n- Add compatibility with `pip>=20.0`\n  ({pr}`953` and\n  {pr}`978`)\n  -- by {user}`atugushev`.\n- Fix a bug where the resolver wouldn't clean up the ephemeral wheel cache ({pr}`968`)\n  -- by {user}`atugushev`.\n\n### Improved Documentation\n\n- Add a note to `README` about `requirements.txt` file, which would possibly interfere\n  if you're compiling from scratch ({pr}`959`)\n  -- by {user}`hramezani`.\n\n## v4.2.0\n\n*12 Oct 2019*\n\n### Features\n\n- Add `--ask` option to `pip-sync` ({pr}`913`)\n  -- by {user}`georgek`.\n\n### Bug Fixes\n\n- Add compatibility with `pip>=19.3`\n  ({pr}`864`,\n  {pr}`904`,\n  {pr}`910`,\n  {pr}`912` and\n  {pr}`915`)\n  -- by {user}`atugushev`.\n- Ensure `pip-compile --no-header <blank requirements.in>` creates/overwrites\n  `requirements.txt` ({pr}`909`)\n  -- by {user}`AndydeCleyre`.\n- Fix `pip-compile --upgrade-package` removes «via» annotation ({pr}`931`)\n  -- by {user}`hramezani`.\n\n### Improved Documentation\n\n- Add info to `README` about layered requirements files and `-c` flag ({pr}`905`)\n  -- by {user}`jamescooke`.\n\n## v4.1.0\n\n*26 Aug 2019*\n\n### Features\n\n- Add `--no-emit-find-links` option to `pip-compile` ({pr}`873`)\n  -- by {user}`jacobtolar`.\n\n### Bug Fixes\n\n- Prevent `--dry-run` log message from being printed with `--quiet` option in\n  `pip-compile` ({pr}`861`)\n  -- by {user}`ddormer`.\n- Fix resolution of requirements from Git URLs without `-e` ({pr}`879`)\n  -- by {user}`andersk`.\n\n## v4.0.0\n\n*25 Jul 2019*\n\n### Backwards Incompatible Changes\n\n- Drop support for EOL Python 3.4 ({pr}`803`)\n  -- by {user}`auvipy`.\n\n### Bug Fixes\n\n- Fix `pip>=19.2` compatibility ({pr}`857`)\n  -- by {user}`atugushev`.\n\n## v3.9.0\n\n*17 Jul 2019*\n\n### Features\n\n- Print provenance information when `pip-compile` fails ({pr}`837`)\n  -- by {user}`jakevdp`.\n\n### Bug Fixes\n\n- Output all logging to stderr instead of stdout ({pr}`834`)\n  -- by {user}`georgek`.\n- Fix output file update with `--dry-run` option in `pip-compile` ({pr}`842`)\n  -- by {user}`shipmints` and.\n  {user}`atugushev`\n\n## v3.8.0\n\n*06 Jun 2019*\n\n### Features\n\n- Options `--upgrade` and `--upgrade-package` are no longer mutually exclusive ({pr}`831`)\n  -- by {user}`adamchainz`.\n\n### Bug Fixes\n\n- Fix `--generate-hashes` with bare VCS URLs ({pr}`812`)\n  -- by {user}`jcushman`.\n- Fix issues with `UnicodeError` when installing `pip-tools` from source in\n  some systems ({pr}`816`) -- by {user}`AbdealiJK`.\n- Respect `--pre` option in the input file ({pr}`822`)\n  -- by {user}`atugushev`.\n- Option `--upgrade-package` now works even if the output file does not exist ({pr}`831`)\n  -- by {user}`adamchainz`.\n\n## v3.7.0\n\n*09 May 2019*\n\n### Features\n\n- Show progressbar on generation hashes in `pip-compile` verbose mode ({pr}`743`)\n  -- by {user}`atugushev`.\n- Add options `--cert` and `--client-cert` to `pip-sync` ({pr}`798`)\n  -- by {user}`atugushev`.\n- Add support for `--find-links` in `pip-compile` output ({pr}`793`)\n  -- by {user}`estan` and {user}`atugushev`.\n- Normalize «command to run» in `pip-compile` headers ({pr}`800`)\n  -- by {user}`atugushev`.\n- Support URLs as packages ({pr}`807`)\n  -- by {user}`jcushman`, {user}`nim65s` and {user}`toejough`.\n\n### Bug Fixes\n\n- Fix replacing password to asterisks in `pip-compile` ({pr}`808`)\n  -- by {user}`atugushev`.\n\n## v3.6.1\n\n*24 Apr 2019*\n\n### Bug Fixes\n\n- Fix `pip>=19.1` compatibility ({pr}`795`)\n  -- by {user}`atugushev`.\n\n## v3.6.0\n\n*03 Apr 2019*\n\n### Features\n\n- Show less output on `pip-sync` with `--quiet` option ({pr}`765`)\n  -- by {user}`atugushev`.\n- Support the flag `--trusted-host` in `pip-sync` ({pr}`777`)\n  -- by {user}`firebirdberlin`.\n\n## v3.5.0\n\n*13 Mar 2019*\n\n### Features\n\n- Show default index url provided by `pip` ({pr}`735`)\n  -- by {user}`atugushev`.\n- Add an option to allow enabling/disabling build isolation ({pr}`758`)\n  -- by {user}`atugushev`.\n\n### Bug Fixes\n\n- Fix the output file for `pip-compile` with an explicit `setup.py` as source\n  file ({pr}`731`) -- by {user}`atugushev`.\n- Fix order issue with generated lock file when `hashes` and `markers` are\n  used together ({pr}`763`) -- by {user}`milind-shakya-sp`.\n\n## v3.4.0\n\n*19 Feb 2019*\n\n### Features\n\n- Add option `--quiet` to `pip-compile` ({pr}`720`)\n  -- by {user}`bendikro`.\n- Emit the original command to the `pip-compile`'s header ({pr}`733`)\n  -- by {user}`atugushev`.\n\n### Bug Fixes\n\n- Fix `pip-sync` to use pip script depending on a python version ({pr}`737`)\n  -- by {user}`atugushev`.\n\n## v3.3.2\n\n*26 Jan 2019*\n\n### Bug Fixes\n\n- Fix `pip-sync` with a temporary requirement file on Windows ({pr}`723`)\n  -- by {user}`atugushev`.\n- Fix `pip-sync` to prevent uninstall of stdlib and dev packages ({pr}`718`)\n  -- by {user}`atugushev`.\n\n## v3.3.1\n\n*24 Jan 2019*\n\n- Re-release of 3.3.0 after fixing the deployment pipeline ({issue}`716`)\n  -- by {user}`atugushev`.\n\n## v3.3.0\n\n*23 Jan 2019*\n\n(Unreleased - Deployment pipeline issue, see 3.3.1)\n\n### Features\n\n- Added support of `pip` 19.0 ({pr}`715`)\n  -- by {user}`atugushev`.\n- Add `--allow-unsafe` to update instructions in the generated\n  `requirements.txt` ({pr}`708`) -- by {user}`richafrank`.\n\n### Bug Fixes\n\n- Fix `pip-sync` to check hashes ({pr}`706`)\n  -- by {user}`atugushev`.\n\n## v3.2.0\n\n*18 Dec 2018*\n\n### Features\n\n- Apply version constraints specified with package upgrade option\n  (`-P, --upgrade-package`) ({pr}`694`)\n  -- by {user}`richafrank`.\n\n## v3.1.0\n\n*05 Oct 2018*\n\n### Features\n\n- Added support of `pip` 18.1 ({pr}`689`)\n  -- by {user}`vphilippon`.\n\n## v3.0.0\n\n*24 Sep 2018*\n\n### Major Changes\n\n- Update `pip-tools` for native `pip` 8, 9, 10 and 18 compatibility,\n  un-vendoring `pip` to use the user-installed `pip` ({pr}`657` and {pr}`672`)\n  -- by {user}`techalchemy`, {user}`suutari`, {user}`tysonclugg` and.\n  {user}`vphilippon`\n\n### Features\n\n- Removed the dependency on the external library `first` ({pr}`676`)\n  -- by {user}`jdufresne`.\n\n## v2.0.2\n\n*28 Apr 2018*\n\n### Bug Fixes\n\n- Added clearer error reporting when skipping pre-releases ({pr}`655`)\n  -- by {user}`WoLpH`.\n\n## v2.0.1\n\n*15 Apr 2018*\n\n### Bug Fixes\n\n- Added missing package data from vendored pip, such as missing cacert.pem file\n  -- by {user}`vphilippon`.\n\n## v2.0.0\n\n*15 Apr 2018*\n\n### Major Changes\n\n- Vendored `pip` 9.0.3 to keep compatibility for users with `pip` 10.0.0 ({pr}`644`)\n  -- by {user}`vphilippon`.\n\n### Features\n\n- Improved the speed of `pip-compile --generate-hashes` by caching the hashes\n  from an existing output file ({pr}`641`)\n  -- by {user}`justicz`.\n- Added a `pip-sync --user` option to restrict attention to user-local\n  directory ({pr}`642`) -- by {user}`jbergknoff-10e`.\n- Removed the hard dependency on setuptools ({pr}`645`)\n  -- by {user}`vphilippon`.\n\n### Bug Fixes\n\n- The pip environment markers on top-level requirements in the source file\n  (requirements.in) are now properly handled and will only be processed in the right\n  environment ({pr}`647`)\n  -- by {user}`JoergRittinger`.\n\n## v1.11.0\n\n*30 Nov 2017*\n\n### Features\n\n- Allow editable packages in requirements.in with\n  `pip-compile --generate-hashes` ({pr}`524`) -- by {user}`jdufresne`.\n- Allow for CA bundles with `pip-compile --cert` ({pr}`612`)\n  -- by {user}`khwilson`.\n- Improved `pip-compile` duration with large locally available editable\n  requirement by skipping a copy to the cache ({pr}`583`)\n  -- by {user}`costypetrisor`.\n- Slightly improved the `NoCandidateFound` error message on potential causes ({pr}`614`)\n  -- by {user}`vphilippon`.\n\n### Bug Fixes\n\n- Add `-markerlib` to the list of `PACKAGES_TO_IGNORE` of `pip-sync` ({pr}`613`).\n\n## v1.10.2\n\n*22 Nov 2017*\n\n### Bug Fixes\n\n- Fixed bug causing dependencies from invalid wheels for the current platform\n  to be included ({pr}`571`).\n- `pip-sync` will respect environment markers in the `requirements.txt` ({pr}`600`)\n  -- by {user}`hazmat345`.\n- Converted the ReadMe to have a nice description rendering on PyPI\n  -- by {user}`bittner`.\n\n## v1.10.1\n\n*27 Sep 2017*\n\n### Bug Fixes\n\n- Fixed bug breaking `pip-sync` on Python 3, raising\n  `TypeError: '<' not supported between instances of 'InstallRequirement' and\n  'InstallRequirement'` ({pr}`570`).\n\n## v1.10.0\n\n*27 Sep 2017*\n\n### Features\n\n- `--generate-hashes` now generates hashes for all wheels, not only wheels for the\n  currently running platform ({pr}`520`)\n  -- by {user}`jdufresne`.\n- Added a `-q`/`--quiet` argument to the `pip-sync` command to reduce log output.\n\n### Bug Fixes\n\n- Fixed bug where unsafe packages would get pinned in generated requirements\n  files when `--allow-unsafe` was not set ({pr}`517`)\n  -- by {user}`dschaller`.\n- Fixed bug where editable PyPI dependencies would have a `download_dir` and be exposed\n  to `git-checkout-index`, (thus losing their VCS directory) and\n  `python setup.py egg_info` fails ({pr}`385`) and {pr}`538`)\n  -- by {user}`blueyed` and {user}`dfee`.\n- Fixed bug where some primary dependencies were annotated with \"via\" info\n  comments ({pr}`542`) -- by {user}`quantus`.\n- Fixed bug where pkg-resources would be removed by `pip-sync` in Ubuntu ({pr}`555`)\n  -- by {user}`cemsbr`.\n- Fixed bug where the resolver would sometime not stabilize on requirements specifying\n  extras ({pr}`566`)\n  -- by {user}`vphilippon`.\n- Fixed an unicode encoding error when distribution package contains non-ASCII file\n  names ({pr}`567`)\n  -- by {user}`suutari`.\n- Fixed package hashing doing unnecessary unpacking ({pr}`557`)\n  -- by {user}`suutari-ai`.\n\n## v1.9.0\n\n*12 Apr 2017*\n\n### Features\n\n- Added ability to read requirements from `setup.py` instead of just `requirements.in`\n  ({pr}`418`)\n  -- by {user}`tysonclugg` and {user}`majuscule`.\n- Added a `--max-rounds` argument to the `pip-compile` command to allow for\n  solving large requirement sets ({pr}`472`)\n  -- by {user}`derek-miller`.\n- Exclude unsafe packages' dependencies when `--allow-unsafe` is not in use ({pr}`441`)\n  -- by {user}`jdufresne`.\n- Exclude irrelevant pip constraints ({pr}`471`)\n  -- by {user}`derek-miller`.\n- Allow control over emitting trusted-host to the compiled requirements ({pr}`448`)\n  -- by {user}`tonyseek`.\n- Allow running as a Python module ({pr}`461`)\n  -- by {user}`AndreLouisCaron`.\n- Preserve environment markers in generated `requirements.txt` ({pr}`460`)\n  -- by {user}`barrywhart`.\n\n### Bug Fixes\n\n- Fixed the `--upgrade-package` option to respect the given package list to\n  update ({pr}`491`).\n- Fixed the default output file name when the source file has no extension\n  ({pr}`488`) -- by {user}`vphilippon`.\n- Fixed crash on editable requirements introduced in 1.8.2.\n- Fixed duplicated `--trusted-host`, `--extra-index-url` and `--index-url` in\n  the generated requirements.\n\n## v1.8.2\n\n*28 Mar 2017*\n\n- Regression fix: editable reqs were losing their dependencies after first\n  round ({pr}`476`) -- by {user}`mattlong`.\n- Remove duplicate index urls in generated `requirements.txt` ({pr}`468`)\n  -- by {user}`majuscule`.\n\n## v1.8.1\n\n*22 Mar 2017*\n\n- Recalculate secondary dependencies between rounds ({pr}`378`)\n- Calculated dependencies could be left with wrong candidates when toplevel requirements\n  happen to be also pinned in sub-dependencies ({pr}`450`)\n- Fix duplicate entries that could happen in generated `requirements.txt` ({pr}`427`)\n- Gracefully report invalid pip version ({pr}`457`)\n- Fix capitalization in the generated `requirements.txt`, packages will always be\n  lowercased ({pr}`452`)\n\n## v1.8.0\n\n*17 Nov 2016*\n\n- Adds support for upgrading individual packages with a new option `--upgrade-package`.\n  To upgrade a _specific_ package to the latest or a specific version use\n  `--upgrade-package <pkg>`. To upgrade all packages, you can still use\n  `pip-compile --upgrade`. ({pr}`409`)\n- Adds support for pinning dependencies even further by including the hashes\n  found on PyPI at compilation time, which will be re-checked when dependencies\n  are installed at installation time. This adds protection against packages\n  that are tampered with. ({pr}`383`)\n- Improve support for extras, like `hypothesis[django]`\n- Drop support for `pip < 8`\n\n## v1.7.1\n\n*20 Oct 2016*\n\n- Add `--allow-unsafe` option (#377)\n\n## v1.7.0\n\n*06 Jul 2016*\n\n- Add compatibility with `pip >= 8.1.2` (#374)\n  -- by {user}`jmbowman`\n\n## v1.6.5\n\n*11 May 2016*\n\n- Add warning that `pip >= 8.1.2` is not supported until 1.7.x is out\n\n## v1.6.4\n\n*03 May 2016*\n\n- Incorporate fix for atomic file saving behaviour on the Windows platform (see {issue}`351`)\n\n## v1.6.3\n\n*02 May 2016*\n\n- PyPI won't let me upload 1.6.2\n\n## v1.6.2\n\n*02 May 2016*\n\n- Respect pip configuration from `pip.{ini,conf}`\n- Fixes for atomic-saving of output files on Windows (see {issue}`351`)\n\n## v1.6.1\n\n*06 Apr 2016*\n\n### Minor Changes\n\n- `pip-sync` now supports being invoked from within and outside an activated virtualenv\n  (see {issue}`317`)\n- `pip-compile`: support `-U` as a shorthand for `--upgrade`\n- `pip-compile`: support pip's `--no-binary` and `--binary-only` flags\n\n### Bug Fixes\n\n- Change header format of output files to mention all input files\n\n## v1.6\n\n*05 Feb 2016*\n\n### Major Changes\n\n- `pip-compile` will by default try to fulfill package specs by looking at a\n  previously compiled output file first, before checking PyPI. This means\n  `pip-compile` will only update the `requirements.txt` when it absolutely has\n  to. To get the old behaviour (picking the latest version of all packages from\n  PyPI), use the new `--upgrade` option.\n\n### Minor Changes\n\n- Bugfix where `pip-compile` would lose \"via\" info when on pip 8 (see {issue}`313`)\n- Ensure cache dir exists (see {issue}`315`)\n\n## v1.5\n\n*23 Jan 2016*\n\n- Add support for `pip >= 8`\n- Drop support for `pip < 7`\n- Fix bug where `pip-sync` fails to uninstall packages if you're using the `--no-index`\n  (or other) flags\n\n## v1.4.5\n\n*20 Jan 2016*\n\n- Add `--no-index` flag to `pip-compile` to avoid emitting `--index-url` into\n  the output (useful if you have configured a different index in your global\n  `~/.pip/pip.conf`, for example)\n- Fix: ignore stdlib backport packages, like `argparse`, when listing which packages\n  will be installed/uninstalled ({issue}`286`)\n- Fix `pip-sync` failed uninstalling packages when using `--find-links` ({issue}`298`)\n- Explicitly error when pip-tools is used with pip 8.0+ (for now)\n\n## v1.4.4\n\n*11 Jan 2016*\n\n- Fix: unintended change in behaviour where packages installed by `pip-sync` could\n  accidentally get upgraded under certain conditions, even though the `requirements.txt`\n  would dictate otherwise (see {issue}`290`)\n\n## v1.4.3\n\n*06 Jan 2016*\n\n- Fix: add `--index-url` and `--extra-index-url` options to `pip-sync`\n- Fix: always install using `--upgrade` flag when running `pip-sync`\n\n## v1.4.2\n\n*13 Dec 2015*\n\n- Fix bug where umask was ignored when writing requirement files ({issue}`268`)\n\n## v1.4.1\n\n*13 Dec 2015*\n\n- Fix bug where successive invocations of `pip-sync` with editables kept\n  uninstalling/installing them (fixes {issue}`270`)\n\n## v1.4.0\n\n*13 Dec 2015*\n\n- Add command line option `-f` / `--find-links`\n- Add command line option `--no-index`\n- Add command line alias `-n` (for `--dry-run`)\n- Fix a unicode issue\n\n## v1.3.0\n\n*08 Dec 2015*\n\n- Support multiple requirement files to `pip-compile`\n- Support requirements from stdin for `pip-compile`\n- Support `--output-file` option on `pip-compile`, to redirect output to a file\n  (or stdout)\n\n## v1.2.0\n\n*30 Nov 2015*\n\n- Add CHANGELOG :)\n- Support pip-sync'ing editable requirements\n- Support extras properly (i.e. `package[foo]` syntax)\n\n(Anything before 1.2.0 was not recorded.)\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Code of Conduct\n\nAs contributors and maintainers of the Jazzband projects, and in the interest\nof fostering an open and welcoming community, we pledge to respect all people\nwho contribute through reporting issues, posting feature requests, updating\ndocumentation, submitting pull requests or patches, and other activities.\n\nWe are committed to making participation in the Jazzband a harassment-free\nexperience for everyone, regardless of the level of experience, gender, gender\nidentity and expression, sexual orientation, disability, personal appearance,\nbody size, race, ethnicity, age, religion, or nationality.\n\nExamples of unacceptable behavior by participants include:\n\n- The use of sexualized language or imagery\n- Personal attacks\n- Trolling or insulting/derogatory comments\n- Public or private harassment\n- Publishing other's private information, such as physical or electronic\n  addresses, without explicit permission\n- Other unethical or unprofessional conduct\n\nThe Jazzband roadies have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\nBy adopting this Code of Conduct, the roadies commit themselves to fairly and\nconsistently applying these principles to every aspect of managing the jazzband\nprojects. Roadies who do not follow or enforce the Code of Conduct may be\npermanently removed from the Jazzband roadies.\n\nThis code of conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community.\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the roadies at `roadies@jazzband.co`. All complaints\nwill be reviewed and investigated and will result in a response that is deemed\nnecessary and appropriate to the circumstances. Roadies are obligated to\nmaintain confidentiality with regard to the reporter of an incident.\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 1.3.0, available at\n[https://contributor-covenant.org/version/1/3/0/][version]\n\n[homepage]: https://contributor-covenant.org\n[version]: https://contributor-covenant.org/version/1/3/0/\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to `pip-tools`\n\n<!-- sphinx-inclusion-post-this-line -->\n\n[![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co)\n\nThis is a [Jazzband](https://jazzband.co) project. By contributing you agree\nto abide by the [Contributor Code of Conduct][coc]\nand follow the [guidelines](https://jazzband.co/about/guidelines).\n\n[coc]: https://jazzband.co/about/conduct\n\n## Project Contribution Guidelines\n\nHere are a few additional or emphasized guidelines to follow when contributing\nto `pip-tools`:\n\n- If you need to have a virtualenv outside of `tox`, it is possible to reuse\n  its configuration to provision it with [tox devenv].\n- Always provide tests for your changes and run `tox -p all` to make sure they\n  are passing the checks locally.\n- Give a clear one-line description in the PR (that the maintainers can add to\n  [CHANGELOG] afterwards).\n- Wait for the review of at least one other contributor before merging (even if\n  you're a Jazzband member).\n- Before merging, assign the PR to a milestone for a version to help with the\n  release process.\n\nThe only exception to those guidelines is for trivial changes, such as\ndocumentation corrections or contributions that do not change pip-tools itself.\n\nContributions following these guidelines are always welcomed, encouraged and\nappreciated.\n\n[tox devenv]: <https://tox.wiki/en/latest/reference/cli.html#tox-devenv-(d)>\n\n### LLM Generated Contributions\n\nContributors are free to use whatever tools they like, but we have some\nadditional guidance for LLM-assisted contributions.\n\nWhen interacting in pip-tools spaces (issues, pull requests, matrix, discord, etc.),\ndo not use LLMs to speak for you, except for translation or grammar edits.\nThis includes the creation of changelogs and PR descriptions.\nHuman-to-human communication is foundational to open source communities.\n\n> [!CAUTION]\n> In extreme cases, low quality PRs may be closed as spam.\n\n#### Responsibility\n\nRemember that you, not the LLM, are responsible for your contributions.\nBe ready to discuss your changes.\nDo not submit code you have not reviewed.\n\nDo your best to follow the conventions and standards of the project.\nMake sure your code really works.\nBe thoughtful about testing and documentation.\n\nTry to make your code brief, and recognize when less is more.\n\n#### Autonomous Code Submissions\n\nThe use of agents which write code and submit pull requests without human review\nis not permitted.\n\n#### Pull Request Templates\n\nPlease do not replace the pull request template, which is part of the\nmaintainers' process.\n\n### The `good first issue` label\n\nThe [`good first issue` label] is used to designate items which are being left\nfor new contributors.\nThey're a great way to get onboarded into the project and learn.\n\nHaving an LLM resolve one of these issues does not help anyone learn.\nTherefore, please be considerate of those who may benefit from these\nopportunities, and refrain from asking an LLM to produce a complete solution.\n\n## Project Release Process\n\nReleases require approval by a member of the [`pip-tools-leads` team].\n\nCommands given below may assume that your fork is named `origin` in git\nremotes and the main repo is named `upstream`.\n\nThis is the current release process:\n\n- Create a branch for the release. _e.g., `release/v3.4.0`_.\n- Use `towncrier` to update the [CHANGELOG], _e.g.,\n  `towncrier build --version v3.4.0`_.\n- Push the branch to your fork, _e.g.,\n  `git push -u origin release/v3.4.0`_, and create a pull request.\n- Merge the pull request after the changes are approved.\n- Make sure that the tests/CI still pass.\n- Fetch the latest changes to `main` locally.\n- Create an unsigned tag with the release version number prefixed with a\n  `v`, _e.g., `git tag -a v3.4.0 -m v3.4.0`_, and push it to `upstream`.\n- Create a GitHub Release, populated with a copy of the changelog and set\n  to \"Create a discussion for this release\" in the `Announcements`\n  category.\n  Some of the markdown will need to be reformatted into GFM.\n  The release title and tag should be the newly created tag.\n- The [GitHub Release Workflow] will trigger off of the release to\n  publish to PyPI. A member of the [`pip-tools-leads` team] must approve\n  the publication step.\n- Once the release to PyPI is confirmed, close the milestone.\n- Publish any release notifications,\n  _e.g., pip-tools matrix channel, discuss.python.org, bluesky, mastodon,\n  pypa Discord_.\n\n[changelog]: ./CHANGELOG.md\n[GitHub Release Workflow]:\nhttps://github.com/jazzband/pip-tools/actions/workflows/release.yml\n[`pip-tools-leads` team]:\nhttps://github.com/orgs/jazzband/teams/pip-tools-leads\n[LLM Policy Discussion]:\nhttps://github.com/jazzband/pip-tools/discussions/2278\n[`good first issue` label]:\nhttps://github.com/jazzband/pip-tools/labels/good%20first%20issue%22\n"
  },
  {
    "path": "LICENSE",
    "content": "BSD 3-Clause License\n\nCopyright (c). All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    1. Redistributions of source code must retain the above copyright notice,\n       this list of conditions and the following disclaimer.\n\n    2. Redistributions in binary form must reproduce the above copyright\n       notice, this list of conditions and the following disclaimer in the\n       documentation and/or other materials provided with the distribution.\n\n    3. Neither the name of pip-tools nor the names of its contributors may be\n       used to endorse or promote products derived from this software without\n       specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "# do not include the `.git_archival.txt` file in sdist builds from repo source\n# this avoids setuptools-scm warnings on development builds\nexclude .git_archival.txt\n"
  },
  {
    "path": "README.md",
    "content": "<!-- pyml disable-next-line first-line-heading -->\n[![jazzband-image]][jazzband]\n[![pypi][pypi-image]][pypi]\n[![pyversions][pyversions-image]][pyversions]\n[![pre-commit][pre-commit-image]][pre-commit]\n[![buildstatus-gha][buildstatus-gha-image]][buildstatus-gha]\n[![codecov][codecov-image]][codecov]\n[![Matrix Room Badge]][Matrix Room]\n[![Matrix Space Badge]][Matrix Space]\n[![discord-chat-image]][discord-chat]\n\n# pip-tools = pip-compile + pip-sync\n\nA set of command line tools to help you keep your `pip`-based packages fresh,\neven when you've pinned them. You do pin them, right? (In building your Python\napplication and its dependencies for production, you want to make sure that\nyour builds are predictable and deterministic.)\n\n[![pip-tools overview for phase II][pip-tools-overview]][pip-tools-overview]\n\n## Installation\n\nSimilar to `pip`, `pip-tools` must be installed in each of your project's\n[virtual environments](https://packaging.python.org/tutorials/installing-packages/#creating-virtual-environments):\n\n```console\n$ source /path/to/venv/bin/activate\n(venv) $ python -m pip install pip-tools\n```\n\n**Note**: all of the remaining example commands assume you've activated your\nproject's virtual environment.\n\n## Example usage for `pip-compile`\n\nThe `pip-compile` command lets you compile a `requirements.txt` file from\nyour dependencies, specified in either `pyproject.toml`, `setup.cfg`,\n`setup.py`, or `requirements.in`.\n\nRun it with `pip-compile` or `python -m piptools compile` (or\n`pipx run --spec pip-tools pip-compile` if `pipx` was installed with the\nappropriate Python version). If you use multiple Python versions, you can also\nrun `py -X.Y -m piptools compile` on Windows and\n`pythonX.Y -m piptools compile` on other systems.\n\n`pip-compile` should be run from the same virtual environment as your\nproject so conditional dependencies that require a specific Python version,\nor other environment markers, resolve relative to your project's\nenvironment.\n\n**Note**: If `pip-compile` finds an existing `requirements.txt` file that\nfulfils the dependencies then no changes will be made, even if updates are\navailable. To compile from scratch, first delete the existing\n`requirements.txt` file, or see\n[Updating requirements](#updating-requirements)\nfor alternative approaches.\n\n### Requirements from `pyproject.toml`\n\nThe `pyproject.toml` file is the\n[latest standard](https://peps.python.org/pep-0621/) for configuring\npackages and applications, and is recommended for new projects. `pip-compile`\nsupports both installing your `project.dependencies` as well as your\n`project.optional-dependencies`. Thanks to the fact that this is an\nofficial standard, you can use `pip-compile` to pin the dependencies\nin projects that use modern standards-adhering packaging tools like\n[Setuptools](https://setuptools.pypa.io), [Hatch](https://hatch.pypa.io/)\nor [flit](https://flit.pypa.io/).\n\nSuppose you have a 'foobar' Python application that is packaged using\n`Setuptools`, and you want to pin it for production. You can declare the\nproject metadata as:\n\n```toml\n[build-system]\nrequires = [\"setuptools\", \"setuptools-scm\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nrequires-python = \">=3.9\"\nname = \"foobar\"\ndynamic = [\"dependencies\", \"optional-dependencies\"]\n\n[tool.setuptools.dynamic]\ndependencies = { file = [\"requirements.in\"] }\noptional-dependencies.test = { file = [\"requirements-test.txt\"] }\n\n```\n\nIf you have a Django application that is packaged using `Hatch`, and you\nwant to pin it for production. You also want to pin your development tools\nin a separate pin file. You declare `django` as a dependency and create an\noptional dependency `dev` that includes `pytest`:\n\n```toml\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\nname = \"my-cool-django-app\"\nversion = \"42\"\ndependencies = [\"django\"]\n\n[project.optional-dependencies]\ndev = [\"pytest\"]\n```\n\nYou can produce your pin files as easily as:\n\n```console\n$ pip-compile -o requirements.txt pyproject.toml\n#\n# This file is autogenerated by pip-compile with Python 3.10\n# by the following command:\n#\n#    pip-compile --output-file=requirements.txt pyproject.toml\n#\nasgiref==3.6.0\n    # via django\ndjango==4.1.7\n    # via my-cool-django-app (pyproject.toml)\nsqlparse==0.4.3\n    # via django\n\n$ pip-compile --extra dev -o dev-requirements.txt pyproject.toml\n#\n# This file is autogenerated by pip-compile with Python 3.10\n# by the following command:\n#\n#    pip-compile --extra=dev --output-file=dev-requirements.txt pyproject.toml\n#\nasgiref==3.6.0\n    # via django\nattrs==22.2.0\n    # via pytest\ndjango==4.1.7\n    # via my-cool-django-app (pyproject.toml)\nexceptiongroup==1.1.1\n    # via pytest\niniconfig==2.0.0\n    # via pytest\npackaging==23.0\n    # via pytest\npluggy==1.0.0\n    # via pytest\npytest==7.2.2\n    # via my-cool-django-app (pyproject.toml)\nsqlparse==0.4.3\n    # via django\ntomli==2.0.1\n    # via pytest\n```\n\nThis is great for both pinning your applications, but also to keep the CI\nof your open-source Python package stable.\n\n### Requirements from `setup.py` and `setup.cfg`\n\n`pip-compile` has also full support for `setup.py`- and\n`setup.cfg`-based projects that use `setuptools`.\n\nJust define your dependencies and extras as usual and run\n`pip-compile` as above.\n\n### Requirements from `requirements.in`\n\nYou can also use plain text files for your requirements (e.g. if you don't\nwant your application to be a package). To use a `requirements.in` file to\ndeclare the Django dependency:\n\n```text\n# requirements.in\ndjango\n```\n\nNow, run `pip-compile requirements.in`:\n\n```console\n$ pip-compile requirements.in\n#\n# This file is autogenerated by pip-compile with Python 3.10\n# by the following command:\n#\n#    pip-compile requirements.in\n#\nasgiref==3.6.0\n    # via django\ndjango==4.1.7\n    # via -r requirements.in\nsqlparse==0.4.3\n    # via django\n```\n\nAnd it will produce your `requirements.txt`, with all the Django dependencies\n(and all underlying dependencies) pinned.\n\n(updating-requirements)=\n\n### Updating requirements\n\n`pip-compile` generates a `requirements.txt` file using the latest versions\nthat fulfil the dependencies you specify in the supported files.\n\nIf `pip-compile` finds an existing `requirements.txt` file that fulfils the\ndependencies then no changes will be made, even if updates are available.\n\nTo force `pip-compile` to update all packages in an existing\n`requirements.txt`, run `pip-compile --upgrade`.\n\nTo update a specific package to the latest or a specific version use the\n`--upgrade-package` or `-P` flag:\n\n```console\n# only update the django package\n$ pip-compile --upgrade-package django\n\n# update both the django and requests packages\n$ pip-compile --upgrade-package django --upgrade-package requests\n\n# update the django package to the latest, and requests to v2.0.0\n$ pip-compile --upgrade-package django --upgrade-package requests==2.0.0\n```\n\nYou can combine `--upgrade` and `--upgrade-package` in one command, to\nprovide constraints on the allowed upgrades. For example to upgrade all\npackages whilst constraining requests to the latest version less than 3.0:\n\n<!-- pyml disable-num-lines 2 commands-show-output -->\n```console\n$ pip-compile --upgrade --upgrade-package 'requests<3.0'\n```\n\n### Understanding output file behavior\n\nWhen you run `pip-compile`, it reads any existing output file (e.g.,\n`requirements.txt`) and uses the versions specified there as constraints.\nThis ensures that the output is stable -- `pip-compile` will prefer to\nuse existing pinned versions.\n\nWhen upgrading packages or adding new packages to the input,\nit is possible for the existing pins to conflict with new packages. In such cases,\n`pip-compile` will update the pins and emit a warning\n(e.g., `Discarding foo=1.2.3 to proceed`).\n\nIn order to resolve dependencies afresh, `pip-compile` provides the following options:\n\n- `--upgrade` / `-U` flag to upgrade all packages\n- `--upgrade-package <package>`/`-P <package>` to upgrade specific packages\n\n### Using hashes\n\nIf you would like to use _Hash-Checking Mode_ available in `pip` since\nversion 8.0, `pip-compile` offers `--generate-hashes` flag:\n\n<!-- pyml disable-num-lines 18 line-length -->\n```console\n$ pip-compile --generate-hashes requirements.in\n#\n# This file is autogenerated by pip-compile with Python 3.10\n# by the following command:\n#\n#    pip-compile --generate-hashes requirements.in\n#\nasgiref==3.6.0 \\\n    --hash=sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac \\\n    --hash=sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506\n    # via django\ndjango==4.1.7 \\\n    --hash=sha256:44f714b81c5f190d9d2ddad01a532fe502fa01c4cb8faf1d081f4264ed15dcd8 \\\n    --hash=sha256:f2f431e75adc40039ace496ad3b9f17227022e8b11566f4b363da44c7e44761e\n    # via -r requirements.in\nsqlparse==0.4.3 \\\n    --hash=sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34 \\\n    --hash=sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268\n    # via django\n```\n\n### Output File\n\nTo output the pinned requirements in a filename other than\n`requirements.txt`, use `--output-file`. This might be useful for compiling\nmultiple files, for example with different constraints on django to test a\nlibrary with both versions using [tox](https://tox.readthedocs.io/en/latest/):\n\n<!-- pyml disable-num-lines 2 commands-show-output -->\n```console\n$ pip-compile --upgrade-package 'django<1.0' --output-file requirements-django0x.txt\n$ pip-compile --upgrade-package 'django<2.0' --output-file requirements-django1x.txt\n```\n\nOr to output to standard output, use `--output-file=-`:\n\n<!-- pyml disable-num-lines 2 commands-show-output -->\n```console\n$ pip-compile --output-file=- > requirements.txt\n$ pip-compile - --output-file=- < requirements.in > requirements.txt\n```\n\n### Forwarding options to `pip`\n\nAny valid `pip` flags or arguments may be passed on with `pip-compile`'s\n`--pip-args` option, e.g.\n\n<!-- pyml disable-num-lines 2 commands-show-output -->\n```console\n$ pip-compile requirements.in --pip-args \"--retries 10 --timeout 30\"\n```\n\n### Configuration\n\nYou can define project-level defaults for `pip-compile` and `pip-sync` by\nwriting them to a configuration file in the same directory as your requirements\ninput files (or the current working directory if piping input from stdin).\nBy default, both `pip-compile` and `pip-sync` will look first\nfor a `.pip-tools.toml` file and then in your `pyproject.toml`. You can\nalso specify an alternate TOML configuration file with the `--config` option.\n\nIt is possible to specify configuration values both globally and command-\nspecific.\nFor example, to by default generate `pip` hashes in the resulting\nrequirements file output, you can specify in a configuration file:\n\n```toml\n[tool.pip-tools]\ngenerate-hashes = true\n```\n\nOptions to `pip-compile` and `pip-sync` that may be used more than once\nmust be defined as lists in a configuration file, even if they only have one\nvalue.\n\n`pip-tools` supports default values for [all valid command-line\nflags](/cli/index.md) of its subcommands. Configuration keys may contain\nunderscores instead of dashes, so the above could also be specified in this\nformat:\n\n```toml\n[tool.pip-tools]\ngenerate_hashes = true\n```\n\nConfiguration defaults specific to `pip-compile` and `pip-sync` can be put\nbeneath separate sections. For example, to by default perform a dry-run with\n`pip-compile`:\n\n```toml\n[tool.pip-tools.compile] # \"sync\" for pip-sync\ndry-run = true\n```\n\nThis does not affect the `pip-sync` command, which also has a `--dry-run`\noption.\nNote that local settings take preference over the global ones of the same name,\nwhenever both are declared, thus this would also make `pip-compile` generate\nhashes, but discard the global dry-run setting:\n\n```toml\n[tool.pip-tools]\ngenerate-hashes = true\ndry-run = true\n\n[tool.pip-tools.compile]\ndry-run = false\n```\n\nYou might be wrapping the `pip-compile` command in another script. To avoid\nconfusing consumers of your custom script you can override the update command\ngenerated at the top of requirements files by setting the\n`CUSTOM_COMPILE_COMMAND` environment variable.\n\n```console\n$ CUSTOM_COMPILE_COMMAND=\"./pipcompilewrapper\" pip-compile requirements.in\n#\n# This file is autogenerated by pip-compile with Python 3.10\n# by the following command:\n#\n#    ./pipcompilewrapper\n#\nasgiref==3.6.0\n    # via django\ndjango==4.1.7\n    # via -r requirements.in\nsqlparse==0.4.3\n    # via django\n```\n\n### Workflow for layered requirements\n\nIf you have different environments that you need to install different but\ncompatible packages for, then you can create layered requirements files and use\none layer to constrain the other.\n\nFor example, if you have a Django project where you want the newest `2.1`\nrelease in production and when developing you want to use the Django debug\ntoolbar, then you can create two `*.in` files, one for each layer:\n\n```text\n# requirements.in\ndjango<2.2\n```\n\nAt the top of the development requirements `dev-requirements.in` you use `-c\nrequirements.txt` to constrain the dev requirements to packages already\nselected for production in `requirements.txt`.\n\n```text\n# dev-requirements.in\n-c requirements.txt\ndjango-debug-toolbar<2.2\n```\n\nFirst, compile `requirements.txt` as usual:\n\n```text\n$ pip-compile\n#\n# This file is autogenerated by pip-compile with Python 3.10\n# by the following command:\n#\n#    pip-compile\n#\ndjango==2.1.15\n    # via -r requirements.in\npytz==2023.3\n    # via django\n```\n\nNow compile the dev requirements and the `requirements.txt` file is used as\na constraint:\n\n```console\n$ pip-compile dev-requirements.in\n#\n# This file is autogenerated by pip-compile with Python 3.10\n# by the following command:\n#\n#    pip-compile dev-requirements.in\n#\ndjango==2.1.15\n    # via\n    #   -c requirements.txt\n    #   django-debug-toolbar\ndjango-debug-toolbar==2.1\n    # via -r dev-requirements.in\npytz==2023.3\n    # via\n    #   -c requirements.txt\n    #   django\nsqlparse==0.4.3\n    # via django-debug-toolbar\n```\n\nAs you can see above, even though a `2.2` release of Django is available, the\ndev requirements only include a `2.1` version of Django because they were\nconstrained. Now both compiled requirements files can be installed safely in\nthe dev environment.\n\nTo install requirements in production stage use:\n\n<!-- pyml disable-num-lines 2 commands-show-output -->\n```console\n$ pip-sync\n```\n\nYou can install requirements in development stage by:\n\n<!-- pyml disable-num-lines 2 commands-show-output -->\n```console\n$ pip-sync requirements.txt dev-requirements.txt\n```\n\n### Version control integration\n\nYou might use `pip-compile` as a hook for the\n[pre-commit](https://github.com/pre-commit/pre-commit).\nSee [pre-commit docs](https://pre-commit.com/) for instructions.\nSample `.pre-commit-config.yaml`:\n\n```yaml\nrepos:\n  - repo: https://github.com/jazzband/pip-tools\n    rev: 7.4.1\n    hooks:\n      - id: pip-compile\n```\n\nYou might want to customize `pip-compile` args by configuring `args` and/or\n`files`, for example:\n\n```yaml\nrepos:\n  - repo: https://github.com/jazzband/pip-tools\n    rev: 7.4.1\n    hooks:\n      - id: pip-compile\n        files: ^requirements/production\\.(in|txt)$\n        args: [--index-url=https://example.com, requirements/production.in]\n```\n\nIf you have multiple requirement files make sure you create a hook for each\nfile.\n\n```yaml\nrepos:\n  - repo: https://github.com/jazzband/pip-tools\n    rev: 7.4.1\n    hooks:\n      - id: pip-compile\n        name: pip-compile setup.py\n        files: ^(setup\\.py|requirements\\.txt)$\n      - id: pip-compile\n        name: pip-compile requirements-dev.in\n        args: [requirements-dev.in]\n        files: ^requirements-dev\\.(in|txt)$\n      - id: pip-compile\n        name: pip-compile requirements-lint.in\n        args: [requirements-lint.in]\n        files: ^requirements-lint\\.(in|txt)$\n      - id: pip-compile\n        name: pip-compile requirements.in\n        args: [requirements.in]\n        files: ^requirements\\.(in|txt)$\n```\n\n### Example usage for `pip-sync`\n\nNow that you have a `requirements.txt`, you can use `pip-sync` to update\nyour virtual environment to reflect exactly what's in there. This will\ninstall/upgrade/uninstall everything necessary to match the\n`requirements.txt` contents.\n\nRun it with `pip-sync` or `python -m piptools sync`. If you use multiple\nPython versions, you can also run `py -X.Y -m piptools sync` on Windows and\n`pythonX.Y -m piptools sync` on other systems.\n\n`pip-sync` must be installed into and run from the same virtual\nenvironment as your project to identify which packages to install\nor upgrade.\n\n**Be careful**: `pip-sync` is meant to be used only with a\n`requirements.txt` generated by `pip-compile`.\n\n```console\n$ pip-sync\nUninstalling flake8-2.4.1:\n    Successfully uninstalled flake8-2.4.1\nCollecting click==4.1\n    Downloading click-4.1-py2.py3-none-any.whl (62kB)\n    100% |................................| 65kB 1.8MB/s\n    Found existing installation: click 4.0\n    Uninstalling click-4.0:\n        Successfully uninstalled click-4.0\nSuccessfully installed click-4.1\n```\n\nTo sync multiple `*.txt` dependency lists, just pass them in via command\nline arguments, e.g.\n\n<!-- pyml disable-num-lines 2 commands-show-output -->\n```console\n$ pip-sync dev-requirements.txt requirements.txt\n```\n\nPassing in empty arguments would cause it to default to `requirements.txt`.\n\nAny valid `pip install` flags or arguments may be passed with `pip-sync`'s\n`--pip-args` option, e.g.\n\n<!-- pyml disable-num-lines 2 commands-show-output -->\n```console\n$ pip-sync requirements.txt --pip-args \"--no-cache-dir --no-deps\"\n```\n\n**Note**: `pip-sync` will not upgrade or uninstall packaging tools like\n`setuptools`, `pip`, or `pip-tools` itself.\nUse `python -m pip install --upgrade` to upgrade those packages.\n\n### Should I commit `requirements.in` and `requirements.txt` to source control?\n\nGenerally, yes. If you want a reproducible environment installation available\nfrom your source control, then yes, you should commit both `requirements.in`\nand `requirements.txt` to source control.\n\nNote that if you are deploying on multiple Python environments (read the\nsection below), then you must commit a separate output file for each Python\nenvironment.\nWe suggest to use the `{env}-requirements.txt` format\n(ex: `win32-py3.7-requirements.txt`, `macos-py3.10-requirements.txt`,\netc.).\n\n### Cross-environment usage of `requirements.in`/`requirements.txt` and `pip-compile`\n\nThe dependencies of a package can change depending on the Python environment in\nwhich it is installed. Here, we define a Python environment as the combination\nof Operating System, Python version (3.7, 3.8, etc.), and Python implementation\n(CPython, PyPy, etc.). For an exact definition, refer to the possible\ncombinations of [PEP 508 environment markers][environment-markers].\n\nAs the resulting `requirements.txt` can differ for each environment, users must\nexecute `pip-compile` **on each Python environment separately** to generate a\n`requirements.txt` valid for each said environment. The same `requirements.in`\ncan be used as the source file for all environments, using\n[PEP 508 environment markers][environment-markers] as needed, the same way it\nwould be done for regular `pip` cross-environment usage.\n\nIf the generated `requirements.txt` remains exactly the same for all Python\nenvironments, then it can be used across Python environments safely. **But**\nusers should be careful as any package update can introduce\nenvironment-dependent dependencies, making any newly generated\n`requirements.txt` environment-dependent too.\nAs a general rule, it's advised that users should still always execute\n`pip-compile` on each targeted Python environment to avoid issues.\n\n### Maximizing reproducibility\n\n`pip-tools` is a great tool to improve the reproducibility of builds.\nBut there are a few things to keep in mind.\n\n- `pip-compile` will produce different results in different environments as\n  described in the previous section.\n- `pip` must be used with the `PIP_CONSTRAINT` environment variable to lock\n  dependencies in build environments as documented in\n  [#8439](https://github.com/pypa/pip/issues/8439).\n- Dependencies come from many sources.\n\nContinuing the `pyproject.toml` example from earlier, creating a single lock\nfile could be done like:\n\n<!-- pyml disable-num-lines 7 line-length -->\n```console\n$ pip-compile --all-build-deps --all-extras --output-file=constraints.txt --strip-extras pyproject.toml\n#\n# This file is autogenerated by pip-compile with Python 3.9\n# by the following command:\n#\n#    pip-compile --all-build-deps --all-extras --output-file=constraints.txt --strip-extras pyproject.toml\n#\nasgiref==3.5.2\n    # via django\nattrs==22.1.0\n    # via pytest\nbackports-zoneinfo==0.2.1\n    # via django\ndjango==4.1\n    # via my-cool-django-app (pyproject.toml)\neditables==0.3\n    # via hatchling\nhatchling==1.11.1\n    # via my-cool-django-app (pyproject.toml::build-system.requires)\niniconfig==1.1.1\n    # via pytest\npackaging==21.3\n    # via\n    #   hatchling\n    #   pytest\npathspec==0.10.2\n    # via hatchling\npluggy==1.0.0\n    # via\n    #   hatchling\n    #   pytest\npy==1.11.0\n    # via pytest\npyparsing==3.0.9\n    # via packaging\npytest==7.1.2\n    # via my-cool-django-app (pyproject.toml)\nsqlparse==0.4.2\n    # via django\ntomli==2.0.1\n    # via\n    #   hatchling\n    #   pytest\n```\n\nSome build backends may also request build dependencies dynamically using the\n`get_requires_for_build_` hooks described in [PEP 517] and [PEP 660].\nThis will be indicated in the output with one of the following suffixes:\n\n- `(pyproject.toml::build-system.backend::editable)`\n- `(pyproject.toml::build-system.backend::sdist)`\n- `(pyproject.toml::build-system.backend::wheel)`\n\n### Other useful tools\n\n- [pip-compile-multi](https://pip-compile-multi.rtfd.io) - pip-compile command\n  wrapper for multiple cross-referencing requirements files.\n- [pipdeptree](https://github.com/tox-dev/pipdeptree) to print the dependency\n  tree of the installed packages.\n- `requirements.in`/`requirements.txt` syntax highlighting:\n  - [requirements.txt.vim](https://github.com/raimon49/requirements.txt.vim)\n    for Vim.\n  - [Python extension for VS Code].\n  - [pip-requirements.el](https://github.com/Wilfred/pip-requirements.el) for\n    Emacs.\n\n[Python extension for VS Code]:\nhttps://marketplace.visualstudio.com/items?itemName=ms-python.python\n\n### Deprecations\n\nThis section lists `pip-tools` features that are currently deprecated.\n\n- In the next major release, the `--allow-unsafe` behavior will be enabled by\n  default (https://github.com/jazzband/pip-tools/issues/989).\n  Use `--no-allow-unsafe` to keep the old behavior. It is recommended\n  to pass `--allow-unsafe` now to adapt to the upcoming change.\n- The legacy resolver is deprecated and will be removed in future versions.\n  The new default is `--resolver=backtracking`.\n- In the next major release, the `--strip-extras` behavior will be enabled by\n  default (https://github.com/jazzband/pip-tools/issues/1613).\n  Use `--no-strip-extras` to keep the old behavior.\n\n### A Note on Resolvers\n\nYou can choose from either default backtracking resolver or the deprecated\nlegacy resolver.\n\nThe legacy resolver will occasionally fail to resolve dependencies. The\nbacktracking resolver is more robust, but can take longer to run in general.\n\nYou can continue using the legacy resolver with `--resolver=legacy` although\nnote that it is deprecated and will be removed in a future release.\n\n[jazzband]: https://jazzband.co/\n[jazzband-image]: https://jazzband.co/static/img/badge.svg\n[pypi]: https://pypi.org/project/pip-tools/\n[pypi-image]: https://img.shields.io/pypi/v/pip-tools.svg\n[pyversions]: https://pypi.org/project/pip-tools/\n[pyversions-image]: https://img.shields.io/pypi/pyversions/pip-tools.svg\n[pre-commit]: https://results.pre-commit.ci/latest/github/jazzband/pip-tools/main\n[pre-commit-image]: https://results.pre-commit.ci/badge/github/jazzband/pip-tools/main.svg\n[buildstatus-gha]: https://github.com/jazzband/pip-tools/actions?query=workflow%3ACI\n[buildstatus-gha-image]: https://github.com/jazzband/pip-tools/workflows/CI/badge.svg\n[codecov]: https://codecov.io/gh/jazzband/pip-tools\n[codecov-image]: https://codecov.io/gh/jazzband/pip-tools/branch/main/graph/badge.svg\n[Matrix Room Badge]: https://img.shields.io/matrix/pip-tools:matrix.org?label=Discuss%20on%20Matrix%20at%20%23pip-tools%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat\n[Matrix Room]: https://matrix.to/#/%23pip-tools:matrix.org\n[Matrix Space Badge]: https://img.shields.io/matrix/jazzband:matrix.org?label=Discuss%20on%20Matrix%20at%20%23jazzband%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat\n[Matrix Space]: https://matrix.to/#/%23jazzband:matrix.org\n[pip-tools-overview]: https://github.com/jazzband/pip-tools/raw/main/img/pip-tools-overview.svg\n[environment-markers]: https://peps.python.org/pep-0508/#environment-markers\n[PEP 517]: https://peps.python.org/pep-0517/\n[PEP 660]: https://peps.python.org/pep-0660/\n[discord-chat]: https://discord.gg/pypa\n[discord-chat-image]: https://img.shields.io/discord/803025117553754132?label=Discord%20chat%20%23pip-tools&style=flat-square\n"
  },
  {
    "path": "changelog.d/.draft_changelog_partial.md",
    "content": "```{towncrier-draft-entries}\nDRAFT_VERSION\n```\n"
  },
  {
    "path": "changelog.d/.gitignore",
    "content": "*\n!.gitignore\n!.towncrier_template.md.jinja\n!.draft_changelog_partial.md\n!README.md\n!*.afterword\n!*.afterword.md\n!*.afterword.*.md\n!*.bugfix\n!*.bugfix.md\n!*.bugfix.*.md\n!*.breaking\n!*.breaking.md\n!*.breaking.*.md\n!*.contrib\n!*.contrib.md\n!*.contrib.*.md\n!*.deprecation\n!*.deprecation.md\n!*.deprecation.*.md\n!*.doc\n!*.doc.md\n!*.doc.*.md\n!*.feature\n!*.feature.md\n!*.feature.*.md\n!*.highlights\n!*.highlights.md\n!*.highlights.*.md\n!*.misc\n!*.misc.md\n!*.misc.*.md\n!*.packaging\n!*.packaging.md\n!*.packaging.*.md\n"
  },
  {
    "path": "changelog.d/.towncrier_template.md.jinja",
    "content": "{%- set is_draft_preview = versiondata[\"version\"] == \"DRAFT_VERSION\" -%}\n{%- set special_category_admonitions = {\n  'afterword': 'tip',\n  'highlights': 'important',\n} -%}\n\n{%- if is_draft_preview -%}\n## Unreleased Changes\n{% else -%}\n## {{ versiondata[\"version\"] }}\n{% endif %}\n\n*{%- if is_draft_preview -%}Generated for preview on {% endif -%}{{ versiondata[\"date\"] }}*\n\n{% for section, _ in sections.items() %}\n{% if sections[section] %}\n{% for category, val in definitions.items() if category in sections[section] -%}\n{%- if category in special_category_admonitions -%}\n:::{{- ['{', '}'] | join(special_category_admonitions[category] | lower) -}}\n{{- '\\n\\n' -}}\n{% for text, change_note_refs in sections[section][category].items() %}\n  {{ text }}\n{% endfor %}\n{{- '\\n' -}}\n:::\n\n{{- '\\n\\n' -}}\n\n{%- else -%}\n\n### {{ definitions[category]['name'] }}\n\n{% for text, change_note_refs in sections[section][category].items() %}\n\n  {%-\n    set pr_issue_numbers = change_note_refs\n    | map('lower')\n    | map('int', default=None)\n    | select('integer')\n    | map('string')\n    | list\n  -%}\n\n  {%- set arbitrary_refs = [] -%}\n  {%- set commit_refs = [] -%}\n  {%- with -%}\n    {%- set commit_ref_candidates = change_note_refs | reject('in', pr_issue_numbers) -%}\n    {%- for cf in commit_ref_candidates -%}\n      {%- if cf | length in (7, 8, 40) and cf | int(default=None, base=16) is not none -%}\n        {%- set _ = commit_refs.append(cf) -%}\n      {%- else -%}\n        {%- set _ = arbitrary_refs.append(cf) -%}\n      {%- endif -%}\n    {%- endfor -%}\n  {%- endwith -%}\n\n- {{ text }}\n\n  {{- '\\n\\n' -}}\n\n  {%- if pr_issue_numbers %}\n  *PRs and issues:* {issue}`{{ pr_issue_numbers | join('`, {issue}`') }}`\n  {{- '\\n' -}}\n  {%- endif -%}\n\n  {%- if commit_refs %}\n  *Related commits:* {commit}`{{ commit_refs | join('`, {commit}`') }}`\n  {{- '\\n' -}}\n  {%- endif -%}\n\n  {%- if arbitrary_refs %}\n  *Unlinked references:* {{ arbitrary_refs | join(', ') }}\n  {{- '\\n' -}}\n  {%- endif -%}\n\n  {{- '\\n' -}}\n\n{% endfor -%}\n{%- endif -%}\n\n{% endfor %}\n{% else %}\nNo significant changes.\n\n{% endif %}\n\n{% endfor %}\n"
  },
  {
    "path": "changelog.d/1142.feature.md",
    "content": "Add usage examples to the ``--help`` output for ``pip-compile`` and\n``pip-sync`` commands.\n\n-- by {user}`Dzhud`\n"
  },
  {
    "path": "changelog.d/2318.contrib.md",
    "content": "`pip-tools` now has a policy regarding LLM-generated contributions, noted in the\ncontributing documentation -- by {user}`sirosen`.\n\nThanks to {user}`0cjs`, {user}`gpshead`, {user}`mr-c`, {user}`samdoran`,\n{user}`webknjaz`, and everyone else in the broader community who helped us\nto craft a policy which is considerate of contributors and protective of the\nproject and its maintainers.\n"
  },
  {
    "path": "changelog.d/2327.contrib.md",
    "content": "Started running {pypi}`zizmor` as a part of CI pipelines to improve\nsecurity of how we configure GitHub Actions CI/CD\n-- by {user}`webknjaz`.\n"
  },
  {
    "path": "changelog.d/2343.contrib.md",
    "content": "pip-tools docs now support GitHub Flavored Markdown admonition blocks\n-- by {user}`webknjaz`.\n"
  },
  {
    "path": "changelog.d/README.md",
    "content": "# How this folder is managed\n\n<!-- sphinx-inclusion-post-this-line -->\n\n## Adding Change Notes with PRs\n\nIt is important to maintain a changelog to explain to users what changed\nbetween versions.\n\nTo avoid merge conflicts, we use\n[Towncrier](https://towncrier.readthedocs.io/en/stable/) to maintain our\nchangelog.\n\nTowncrier uses separate files, \"news fragments\", for each pull request.\nOn release, those fragments are compiled into the changelog.\n\nYou don't need to install Towncrier to contribute, you just have to follow some\nsimple rules!\n\n- In your pull request, add a new file into `changelog.d/` with a filename\n  formatted as `$NUMBER.$CATEGORY.md`.\n\n  - The number is the PR number or issue number which your PR addresses.\n\n  - The category is `bugfix`, `feature`, `deprecation`, `breaking`, `doc`,\n    `packaging`, `contrib`, or `misc`. The maintainers may use special\n    categories `highlights` and `afterword`, but regular contributors\n    don't generally need to.\n\n  - For example, if your PR fixes bug #404, the change notes should be named\n    `changelog.d/404.bugfix.md`.\n\n- If multiple issues are addressed, create a symlink to the change notes with\n  another issue number in the name.\n  Towncrier will automatically merge files into one entry with multiple links.\n\n- Prefer the simple past or constructions with \"now\".\n\n- Include a byline, `` -- by {user}`github-username` ``\n\nYou can preview the changelog by running `tox run -e build-docs` and viewing\nthe changelog in the docs.\n\n### Categories\n\nThe categories for change notes are defined as follows.\n\n- `bugfix`: A fix for something we deemed improper or undesired behavior.\n\n- `feature`: A new behavior, such as a new flag or environment variable.\n\n- `deprecation`: A declaration of future removals and breaking changes in\n  behavior.\n\n- `breaking`: A change in behavior which changes or violates established user\n  expectations\n  (e.g., removing a flag or changing output formatting).\n\n- `doc`: Notable updates to the documentation structure or build process.\n\n- `packaging`: Changes in how `pip-tools` itself is packaged and tested which\n  may impact downstreams and redistributors.\n\n- `contrib`: Changes to the contributor experience\n  (e.g., running tests, building the docs, or setting up a development\n  environment).\n\n- `misc`: Changes that don't fit any of the other categories.\n\n- `highlights`: A prelude to the release notes. Normally filled out by\n  the maintainers with important announcements concerning a `pip-tools`\n  release. It is rendered before the normal change log categories.\n\n  > [!IMPORTANT]\n  > The file name must not reference a number but be orphan, starting\n  > with a `+`.\n\n- `afterword`: A concluding paragraph after the release notes. May be\n  filled out by the maintainers if they want to include additional\n  free-form notes. It is rendered after the normal change log categories.\n\n  > [!IMPORTANT]\n  > The file name must not reference a number but be orphan, starting\n  > with a `+`.\n\nSometimes it's not clear which category to use for a change.\nDo your best and a maintainer can discuss this with you during review.\n\n### Examples\n\nExample bugfix, [`2223.bugfix.md`]:\n\n```md\nFixed a bug which removed slashes from URLs in `-r` and `-c` in the output\nof `pip-compile` -- by {user}`sirosen`.\n```\n\nExample contributor update, [`2214.contrib.md`]:\n\n```md\n`pip-tools` now tests on and officially supports `pip` version 25.2\n-- by {user}`sirosen`.\n```\n\n[`2223.bugfix.md`]: https://github.com/jazzband/pip-tools/pull/2224\n[`2214.contrib.md`]: https://github.com/jazzband/pip-tools/pull/2214\n\n### Rationale\n\nWhen making a change to `pip-tools`, it is important to communicate the\ndifferences that end-users will experience in a manner that they can\nunderstand.\n\nDetails of the change that are primarily of interest only to `pip-tools`\ndevelopers may be irrelevant to most users, and if so, then those details can\nbe omitted from the change notes.\nThen, when the maintainers publish a new release, they'll automatically use\nthese records to compose a change log for the respective version.\n\nWe write change notes in the past tense because this suits the users who will\nbe reading these notes.\nCombined with others, the notes will be a part of the \"news digest\" telling the\nreaders what **changed** in a specific version of `pip-tools` since the\nprevious version.\n\nThis methodology has several benefits, including those covered by the\n[Towncrier Philosophy](https://towncrier.readthedocs.io/en/stable/#philosophy):\n\n- Change notes separate the user-facing description of changes from the\n  implementation details.\n  Details go into the git history, but users aren't expected to care about\n  them.\n\n- The release engineer may not have been involved in each issue and pull\n  request.\n  Writing the notes early in the process involves the developers in the best\n  position to write good notes.\n\n- Describing a change can help during code review.\n  The reviewer can better identify which effects of a change were intentional\n  and which were not.\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "!requirements.in\n!requirements.txt\n"
  },
  {
    "path": "docs/changelog.md",
    "content": "# Changelog\n\n```{eval-rst}\n\n.. MyST doesn't support the \"only\" directive correctly. It always evaluates to\n.. true.\n..\n.. But if we drop into sphinx eval-rst, it works fine.\n.. We then need to include our draft changelog content as markdown.\n..\n.. We're making a \"MyST sandwich\", with RST for the `{only}` directive in the\n.. middle.\n..\n.. Using `include` with a `parser` is documented here:\n.. https://myst-parser.readthedocs.io/en/latest/faq/index.html#include-rst-files-into-a-markdown-file\n\n.. only:: not is_release\n\n    .. include:: ../changelog.d/.draft_changelog_partial.md\n        :parser: myst_parser.sphinx_\n\n```\n\n```{include} ../CHANGELOG.md\n:start-after: <!-- towncrier release notes start -->\n```\n"
  },
  {
    "path": "docs/cli/index.md",
    "content": "# Command Line Reference\n\nThis page provides a reference for the `pip-tools` command-line interface (CLI):\n\n```{toctree}\n:maxdepth: 1\n\npip-compile\npip-sync\n```\n"
  },
  {
    "path": "docs/cli/pip-compile.md",
    "content": "# pip-compile\n\n```{program-output} pip-compile --help\n\n```\n"
  },
  {
    "path": "docs/cli/pip-sync.md",
    "content": "# pip-sync\n\n```{program-output} pip-sync --help\n\n```\n"
  },
  {
    "path": "docs/conf.py",
    "content": "# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\"\"\"Configuration file for the Sphinx documentation builder.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom importlib.metadata import version as get_version\nfrom pathlib import Path\n\nfrom sphinx.application import Sphinx\nfrom sphinx.util import logging\nfrom sphinx.util.console import bold\n\nlogger = logging.getLogger(__name__)\n\nPROJECT_ROOT_DIR = Path(__file__).parents[1].resolve()\nIS_RELEASE_ON_RTD = (\n    os.getenv(\"READTHEDOCS\", \"False\") == \"True\"\n    and os.environ[\"READTHEDOCS_VERSION_TYPE\"] == \"tag\"\n)\n\n\n# -- Project information -----------------------------------------------------\n\nproject = \"pip-tools\"\nauthor = f\"{project} Contributors\"\ncopyright = f\"The {author}\"\n\n# The full version, including alpha/beta/rc tags\nrelease = get_version(project)\n\n# The short X.Y version\nversion = \".\".join(release.split(\".\")[:3])\n\nlogger.info(bold(\"%s version: %s\"), project, version)\nlogger.info(bold(\"%s release: %s\"), project, release)\n\n# -- General configuration ---------------------------------------------------\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    # Stdlib extensions:\n    \"sphinx.ext.intersphinx\",\n    # Third-party extensions:\n    \"click_extra.sphinx\",  # provides GitHub-flavored admonition syntax\n    \"myst_parser\",\n    \"sphinxcontrib.apidoc\",\n    \"sphinxcontrib.programoutput\",\n    \"sphinxcontrib.towncrier.ext\",  # provides `.. towncrier-draft-entries::`\n    \"sphinx_issues\",\n]\n\n# -- Options for HTML output -------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\nhtml_theme = \"furo\"\nhtml_title = f\"<nobr>{project}</nobr> documentation v{release}\"\n\n\n# -- Options for intersphinx ----------------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration\n\nintersphinx_mapping = {\n    \"python\": (\"https://docs.python.org/3\", None),\n}\n\nissues_github_path = \"jazzband/pip-tools\"\n\ntowncrier_draft_autoversion_mode = \"draft\"\ntowncrier_draft_include_empty = True\ntowncrier_draft_working_directory = PROJECT_ROOT_DIR\ntowncrier_draft_config_path = \"towncrier.toml\"  # relative to cwd\n\n# -------------------------------------------------------------------------\ndefault_role = \"any\"\nnitpicky = True\n\nlinkcheck_ignore = [\n    r\"^https://matrix\\.to/#\",\n    r\"^https://img.shields.io/matrix\",\n    r\"^https://results\\.pre-commit\\.ci/latest/github/jazzband/pip-tools/\",\n    # checking sphinx-issues links to GitHub results in rate limiting errors\n    # skip any username validation and pip-tools link checking\n    # (this also means we won't get spurious errors when users delete their GitHub accounts)\n    r\"^https://github\\.com/jazzband/pip-tools/(issues|pull|commit)/\",\n    r\"^https://github\\.com/sponsors/\",\n]\n\nnitpick_ignore_regex = [\n    (\"py:class\", \"pip.*\"),\n    (\"py:class\", \"pathlib.*\"),\n    (\"py:class\", \"click.*\"),\n    (\"py:class\", \"build.*\"),\n    (\"py:class\", \"optparse.*\"),\n    (\"py:class\", \"_ImportLibDist\"),\n    (\"py:class\", \"PackageMetadata\"),\n    (\"py:class\", \"importlib.*\"),\n    (\"py:class\", \"IndexContent\"),\n    (\"py:exc\", \"click.*\"),\n]\n\nsuppress_warnings = [\n    \"myst.xref_missing\",\n    # MyST erroneously flags the draft changelog as having improper header levels\n    # because it starts at H2 instead of H1.\n    # However, it is written only for inclusion in a broader doc, so the heading\n    # levels are actually correct.\n    \"myst.header\",\n]\n\n# -- Apidoc options -------------------------------------------------------\n\napidoc_excluded_paths: list[str] = []\napidoc_extra_args = [\n    \"--implicit-namespaces\",\n    \"--private\",  # include “_private” modules\n]\napidoc_module_first = False\napidoc_module_dir = \"../piptools\"\napidoc_output_dir = \"pkg\"\napidoc_separate_modules = True\napidoc_toc_file = None\n\n\n# -- myst_parser options --------------------------------------------------\nmyst_enable_extensions = {\n    \"colon_fence\",\n}\n\n\n# -- Sphinx extension-API `setup()` hook\n\n\ndef setup(app: Sphinx) -> dict[str, bool | str]:\n    \"\"\"Register project-local Sphinx extension-API customizations.\n\n    :param app: Initialized Sphinx app instance.\n    :returns: Extension metadata.\n    \"\"\"\n    if IS_RELEASE_ON_RTD:\n        app.tags.add(\"is_release\")\n\n    return {\n        \"parallel_read_safe\": True,\n        \"parallel_write_safe\": True,\n        \"version\": release,\n    }\n"
  },
  {
    "path": "docs/contributing.md",
    "content": "# Contributing\n\n```{include} ../CONTRIBUTING.md\n:start-after: <!-- sphinx-inclusion-post-this-line -->\n```\n\n```{include} ../changelog.d/README.md\n:start-after: <!-- sphinx-inclusion-post-this-line -->\n```\n"
  },
  {
    "path": "docs/index.md",
    "content": "<!-- pyml disable-next-line no-trailing-punctuation -->\n# Welcome to pip-tools' documentation!\n\n```{include} ../README.md\n\n```\n\n```{toctree}\n:hidden:\n:maxdepth: 2\n:caption: Contents\n\ncli/index\ncontributing\nchangelog\n```\n\n```{toctree}\n:hidden:\n:caption: Private API reference\n\npkg/modules\n```\n"
  },
  {
    "path": "docs/pkg/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "docs/requirements.in",
    "content": "click-extra [sphinx]  # GitHub-flavored admonitions: https://github.com/executablebooks/MyST-Parser/issues/845#issuecomment-3580549737\nfuro\nmyst-parser\nsetuptools-scm\nsphinx\nsphinxcontrib-apidoc\nsphinxcontrib-programoutput\nsphinxcontrib-towncrier\nsphinx-issues\n"
  },
  {
    "path": "docs/requirements.txt",
    "content": "#\n# This file is autogenerated by pip-compile with Python 3.12\n# by the following command:\n#\n#    pip-compile --allow-unsafe --output-file=docs/requirements.txt --strip-extras ./pyproject.toml docs/requirements.in\n#\naccessible-pygments==0.0.5\n    # via furo\nalabaster==1.0.0\n    # via sphinx\nbabel==2.18.0\n    # via sphinx\nbeautifulsoup4==4.14.3\n    # via furo\nboltons==25.0.0\n    # via click-extra\nbracex==2.6\n    # via wcmatch\nbuild==1.4.0\n    # via pip-tools (pyproject.toml)\ncertifi==2026.1.4\n    # via requests\ncharset-normalizer==3.4.4\n    # via requests\nclick==8.3.1\n    # via\n    #   click-extra\n    #   cloup\n    #   pip-tools (pyproject.toml)\n    #   towncrier\nclick-extra==7.5.3\n    # via -r docs/requirements.in\ncloup==3.0.8\n    # via click-extra\ndeepmerge==2.0\n    # via click-extra\ndistro==1.9.0\n    # via extra-platforms\ndocutils==0.22.4\n    # via\n    #   click-extra\n    #   myst-parser\n    #   sphinx\nextra-platforms==8.0.0\n    # via click-extra\nfuro==2025.12.19\n    # via -r docs/requirements.in\nidna==3.11\n    # via requests\nimagesize==1.4.1\n    # via sphinx\njinja2==3.1.6\n    # via\n    #   myst-parser\n    #   sphinx\n    #   towncrier\nmarkdown-it-py==4.0.0\n    # via\n    #   mdit-py-plugins\n    #   myst-parser\nmarkupsafe==3.0.3\n    # via jinja2\nmdit-py-plugins==0.5.0\n    # via myst-parser\nmdurl==0.1.2\n    # via markdown-it-py\nmyst-parser==5.0.0\n    # via -r docs/requirements.in\npackaging==26.0\n    # via\n    #   build\n    #   setuptools-scm\n    #   sphinx\n    #   wheel\npbr==7.0.3\n    # via sphinxcontrib-apidoc\npygments==2.19.2\n    # via\n    #   accessible-pygments\n    #   click-extra\n    #   furo\n    #   pygments-ansi-color\n    #   sphinx\npygments-ansi-color==0.3.0\n    # via click-extra\npyproject-hooks==1.2.0\n    # via\n    #   build\n    #   pip-tools (pyproject.toml)\npyyaml==6.0.3\n    # via myst-parser\nrequests==2.32.5\n    # via\n    #   click-extra\n    #   sphinx\nroman-numerals==4.1.0\n    # via sphinx\nsetuptools-scm==9.2.2\n    # via -r docs/requirements.in\nsnowballstemmer==3.0.1\n    # via sphinx\nsoupsieve==2.8.3\n    # via beautifulsoup4\nsphinx==9.1.0\n    # via\n    #   -r docs/requirements.in\n    #   click-extra\n    #   furo\n    #   myst-parser\n    #   sphinx-basic-ng\n    #   sphinx-issues\n    #   sphinxcontrib-apidoc\n    #   sphinxcontrib-programoutput\n    #   sphinxcontrib-towncrier\nsphinx-basic-ng==1.0.0b2\n    # via furo\nsphinx-issues==5.0.1\n    # via -r docs/requirements.in\nsphinxcontrib-apidoc==0.6.0\n    # via -r docs/requirements.in\nsphinxcontrib-applehelp==2.0.0\n    # via sphinx\nsphinxcontrib-devhelp==2.0.0\n    # via sphinx\nsphinxcontrib-htmlhelp==2.1.0\n    # via sphinx\nsphinxcontrib-jsmath==1.0.1\n    # via sphinx\nsphinxcontrib-programoutput==0.18\n    # via -r docs/requirements.in\nsphinxcontrib-qthelp==2.0.0\n    # via sphinx\nsphinxcontrib-serializinghtml==2.0.0\n    # via sphinx\nsphinxcontrib-towncrier==0.5.0a0\n    # via -r docs/requirements.in\ntabulate==0.9.0\n    # via click-extra\ntowncrier==25.8.0\n    # via sphinxcontrib-towncrier\ntyping-extensions==4.15.0\n    # via beautifulsoup4\nurllib3==2.6.3\n    # via requests\nwcmatch==10.1\n    # via click-extra\nwcwidth==0.5.3\n    # via tabulate\nwheel==0.46.3\n    # via pip-tools (pyproject.toml)\n\n# The following packages are considered to be unsafe in a requirements file:\npip==26.0\n    # via pip-tools (pyproject.toml)\nsetuptools==80.10.2\n    # via\n    #   pbr\n    #   pip-tools (pyproject.toml)\n    #   setuptools-scm\n"
  },
  {
    "path": "examples/django.in",
    "content": "# This file includes the Django project, and the debug toolbar\nDjango<2.2.1   # suppose some version requirement\ndjango-debug-toolbar\n"
  },
  {
    "path": "examples/flask.in",
    "content": "# Flask has 2nd and 3rd level dependencies\nFlask\n"
  },
  {
    "path": "examples/hypothesis.in",
    "content": "hypothesis[django]\n"
  },
  {
    "path": "examples/protection.in",
    "content": "# This package depends on setuptools, which should not end up in the compiled\n# requirements, because it may cause conflicts with pip itself\npython-levenshtein>=0.12.0\n"
  },
  {
    "path": "examples/readme/constraints.txt",
    "content": "#\n# This file is autogenerated by pip-compile with Python 3.11\n# by the following command:\n#\n#    pip-compile --all-build-deps --all-extras --output-file=constraints.txt --strip-extras pyproject.toml\n#\nasgiref==3.5.2\n    # via django\nattrs==22.1.0\n    # via pytest\ndjango==4.1\n    # via my-cool-django-app (pyproject.toml)\neditables==0.3\n    # via hatchling\nhatchling==1.11.1\n    # via my-cool-django-app (pyproject.toml::build-system.requires)\niniconfig==1.1.1\n    # via pytest\npackaging==21.3\n    # via\n    #   hatchling\n    #   pytest\npathspec==0.10.2\n    # via hatchling\npluggy==1.0.0\n    # via\n    #   hatchling\n    #   pytest\npy==1.11.0\n    # via pytest\npyparsing==3.0.9\n    # via packaging\npytest==7.1.2\n    # via my-cool-django-app (pyproject.toml)\nsqlparse==0.4.2\n    # via django\ntomli==2.0.1\n    # via\n    #   hatchling\n    #   pytest\n"
  },
  {
    "path": "examples/readme/pyproject.toml",
    "content": "[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\nname = \"my-cool-django-app\"\nversion = \"42\"\ndependencies = [\"django\"]\n\n[project.optional-dependencies]\ndev = [\"pytest\"]\n"
  },
  {
    "path": "examples/sentry.in",
    "content": "# Sentry has a very large dependency tree\nsentry\n"
  },
  {
    "path": "piptools/__init__.py",
    "content": "from __future__ import annotations\n\nimport locale\n\nfrom click import secho\n\n# Needed for locale.getpreferredencoding(False) to work\n# in pip._internal.utils.encoding.auto_decode\ntry:\n    locale.setlocale(locale.LC_ALL, \"\")\nexcept locale.Error as e:  # pragma: no cover\n    # setlocale can apparently crash if locale are uninitialized\n    secho(f\"Ignoring error when setting locale: {e}\", fg=\"red\")\n"
  },
  {
    "path": "piptools/__main__.py",
    "content": "from __future__ import annotations\n\nimport click\n\nfrom piptools.scripts import compile, sync\n\n\n@click.group()\ndef cli() -> None:\n    pass\n\n\ncli.add_command(compile.cli, \"compile\")\ncli.add_command(sync.cli, \"sync\")\n\n\n# Enable ``python -m piptools ...``.\nif __name__ == \"__main__\":\n    cli()\n"
  },
  {
    "path": "piptools/_compat/__init__.py",
    "content": "from __future__ import annotations\n\nfrom .pip_compat import (\n    Distribution,\n    canonicalize_name,\n    create_wheel_cache,\n    get_dev_pkgs,\n    parse_requirements,\n)\n\n__all__ = [\n    \"Distribution\",\n    \"parse_requirements\",\n    \"create_wheel_cache\",\n    \"get_dev_pkgs\",\n    \"canonicalize_name\",\n]\n"
  },
  {
    "path": "piptools/_compat/_tomllib_compat.py",
    "content": "from __future__ import annotations\n\nimport sys\n\nif sys.version_info >= (3, 11):\n    from tomllib import TOMLDecodeError, load, loads\nelse:\n    from tomli import TOMLDecodeError, load, loads\n\n\n__all__ = [\"TOMLDecodeError\", \"load\", \"loads\"]\n"
  },
  {
    "path": "piptools/_compat/path_compat.py",
    "content": "\"\"\"\nCompatibility helpers for working with paths and :mod:`pathlib` across platforms\nand Python versions.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os.path\nimport pathlib\nimport sys\n\n__all__ = (\"relative_to_walk_up\",)\n\n\ndef relative_to_walk_up(path: pathlib.Path, start: pathlib.Path) -> pathlib.Path:\n    \"\"\"\n    Compute a relative path allowing for the input to not be a subpath of the start.\n\n    This is a compatibility helper for ``pathlib.Path.relative_to(..., walk_up=True)``\n    on all Python versions. (``walk_up: bool`` is Python 3.12+)\n    \"\"\"\n    # prefer `pathlib.Path.relative_to` where available\n    if sys.version_info >= (3, 12):\n        return path.relative_to(start, walk_up=True)\n\n    str_result = os.path.relpath(path, start=start)\n    return pathlib.Path(str_result)\n"
  },
  {
    "path": "piptools/_compat/pip_compat.py",
    "content": "from __future__ import annotations\n\nimport optparse\nimport pathlib\nimport typing as _t\nimport urllib.parse\nfrom collections.abc import Iterable, Iterator\nfrom dataclasses import dataclass\n\nfrom pip._internal.cache import WheelCache\nfrom pip._internal.index.package_finder import PackageFinder\nfrom pip._internal.metadata import BaseDistribution\nfrom pip._internal.metadata.pkg_resources import Distribution as _PkgResourcesDist\nfrom pip._internal.models.direct_url import DirectUrl\nfrom pip._internal.models.link import Link\nfrom pip._internal.network.session import PipSession\nfrom pip._internal.req import InstallRequirement\nfrom pip._internal.req import parse_requirements as _parse_requirements\nfrom pip._internal.req.constructors import install_req_from_parsed_requirement\nfrom pip._vendor.pkg_resources import Requirement\n\nfrom .path_compat import relative_to_walk_up\n\n# The Distribution interface has changed between pkg_resources and\n# importlib.metadata, so this compat layer allows for a consistent access\n# pattern. In pip 22.1, importlib.metadata became the default on Python 3.11\n# (and later), but is overridable. `select_backend` returns what's being used.\n# Secondly, the canonicalize_name function received typing improvements\n# in pip 21.2, since mypy runs on an older version, this compat layer ensures correct\n# typing regardless of the pip version used. NormalizedName and str are interchangeable.\nif _t.TYPE_CHECKING:\n    from pip._internal.metadata.importlib import Distribution as _ImportLibDist\n\n    def canonicalize_name(name: str) -> str: ...\n\nelse:\n    from pip._vendor.packaging.utils import canonicalize_name  # noqa: F401\n\nfrom .._internal import _pip_api\n\n\n@dataclass(frozen=True)\nclass Distribution:\n    key: str\n    version: str\n    requires: Iterable[Requirement]\n    direct_url: DirectUrl | None\n\n    @classmethod\n    def from_pip_distribution(cls, dist: BaseDistribution) -> Distribution:\n        # TODO: Use only the BaseDistribution protocol properties and methods\n        # instead of specializing by type.\n        if isinstance(dist, _PkgResourcesDist):\n            return cls._from_pkg_resources(dist)\n        else:\n            return cls._from_importlib(dist)\n\n    @classmethod\n    def _from_pkg_resources(cls, dist: _PkgResourcesDist) -> Distribution:\n        return cls(\n            dist._dist.key, dist._dist.version, dist._dist.requires(), dist.direct_url\n        )\n\n    @classmethod\n    def _from_importlib(cls, dist: _ImportLibDist) -> Distribution:\n        \"\"\"Mimic pkg_resources.Distribution.requires for the case of no\n        extras.\n\n        This doesn't fulfill that API's ``extras`` parameter but\n        satisfies the needs of pip-tools.\n        \"\"\"\n        reqs = (Requirement.parse(req) for req in (dist._dist.requires or ()))\n        requires = [\n            req\n            for req in reqs\n            if not req.marker or req.marker.evaluate({\"extra\": None})\n        ]\n        return cls(dist._dist.name, dist._dist.version, requires, dist.direct_url)\n\n\nclass FileLink(Link):  # type: ignore[misc]\n    \"\"\"Wrapper for ``pip``'s ``Link`` class.\"\"\"\n\n    _url: str\n\n    @property\n    def file_path(self) -> str:\n        # overriding the actual property to bypass some validation\n        return self._url\n\n\ndef parse_requirements(\n    filename: str,\n    session: PipSession,\n    finder: PackageFinder | None = None,\n    options: optparse.Values | None = None,\n    constraint: bool = False,\n    isolated: bool = False,\n    comes_from_stdin: bool = False,\n) -> Iterator[InstallRequirement]:\n    # the `comes_from` data will be rewritten in different ways in different conditions\n    # each rewrite rule is expressible as a str->str function\n    rewrite_comes_from: _t.Callable[[str], str]\n\n    if comes_from_stdin:\n        # if data is coming from stdin, then `comes_from=\"-r -\"`\n        rewrite_comes_from = _rewrite_comes_from_to_hardcoded_stdin_value\n    elif pathlib.Path(filename).is_absolute():\n        # if the input path is absolute, just normalize paths to posix-style\n        rewrite_comes_from = _normalize_comes_from_location\n    else:\n        # if the input was a relative path, set the rewrite rule to rewrite\n        # absolute paths to be relative\n        rewrite_comes_from = _relativize_comes_from_location\n\n    for parsed_req in _parse_requirements(\n        filename, session, finder=finder, options=options, constraint=constraint\n    ):\n        install_req = install_req_from_parsed_requirement(parsed_req, isolated=isolated)\n        if install_req.editable and not parsed_req.requirement.startswith(\"file://\"):\n            # ``Link.url`` is what is saved to the output file\n            # we set the url directly to undo the transformation in pip's Link class\n            file_link = FileLink(install_req.link.url)\n            file_link._url = parsed_req.requirement\n            install_req.link = file_link\n        install_req = _pip_api.copy_install_requirement(install_req)\n\n        install_req.comes_from = rewrite_comes_from(install_req.comes_from)\n\n        yield install_req\n\n\ndef _rewrite_comes_from_to_hardcoded_stdin_value(_: str, /) -> str:\n    \"\"\"Produce the hardcoded ``comes_from`` value for stdin.\"\"\"\n    return \"-r -\"\n\n\ndef _relativize_comes_from_location(original_comes_from: str, /) -> str:\n    \"\"\"\n    Convert a ``comes_from`` path to a relative posix path.\n\n    This is the rewrite rule used when ``-r`` or ``-c`` appears in\n    ``comes_from`` data with an absolute path.\n\n    The ``-r`` or ``-c`` qualifier is retained, the path is relativized\n    with respect to the CWD, and the path is converted to posix style.\n    \"\"\"\n    # require `-r` or `-c` as the source\n    if not original_comes_from.startswith((\"-r \", \"-c \")):\n        return original_comes_from\n\n    # split on the space\n    prefix, space_sep, suffix = original_comes_from.partition(\" \")\n\n    # if the value part is a remote URI for pip, return the original\n    if _is_remote_pip_uri(suffix):\n        return original_comes_from\n\n    file_path = pathlib.Path(suffix)\n\n    # if the path was not absolute, normalize to posix-style and finish processing\n    if not file_path.is_absolute():\n        return f\"{prefix} {file_path.as_posix()}\"\n\n    # make it relative to the current working dir\n    suffix = relative_to_walk_up(file_path, pathlib.Path.cwd()).as_posix()\n    return f\"{prefix}{space_sep}{suffix}\"\n\n\ndef _normalize_comes_from_location(original_comes_from: str, /) -> str:\n    \"\"\"\n    Convert a ``comes_from`` path to a posix-style path.\n\n    This is the rewrite rule when ``-r`` or ``-c`` appears in ``comes_from``\n    data and the input path was absolute, meaning we should not relativize the\n    locations.\n\n    The ``-r`` or ``-c`` qualifier is retained, and the path is converted to\n    posix style.\n    \"\"\"\n    # require `-r` or `-c` as the source\n    if not original_comes_from.startswith((\"-r \", \"-c \")):\n        return original_comes_from\n\n    # split on the space\n    prefix, space_sep, suffix = original_comes_from.partition(\" \")\n\n    # if the value part is a remote URI for pip, return the original\n    if _is_remote_pip_uri(suffix):\n        return original_comes_from\n\n    # convert to a posix-style path\n    suffix = pathlib.Path(suffix).as_posix()\n    return f\"{prefix}{space_sep}{suffix}\"\n\n\ndef _is_remote_pip_uri(value: str) -> bool:\n    \"\"\"\n    Test a string to see if it is a URI treated as a remote file in ``pip``.\n    Specifically this means that it's a 'file', 'http', or 'https' URI.\n\n    The test is performed by trying a URL parse and reading the scheme.\n    \"\"\"\n    scheme = urllib.parse.urlsplit(value).scheme\n    return scheme in {\"file\", \"http\", \"https\"}\n\n\ndef create_wheel_cache(cache_dir: str, format_control: str | None = None) -> WheelCache:\n    kwargs: dict[str, str | None] = {\"cache_dir\": cache_dir}\n    if _pip_api.PIP_VERSION_MAJOR_MINOR <= (23, 0):\n        kwargs[\"format_control\"] = format_control\n    return WheelCache(**kwargs)\n\n\ndef get_dev_pkgs() -> set[str]:\n    if _pip_api.PIP_VERSION_MAJOR_MINOR <= (23, 1):\n        from pip._internal.commands.freeze import DEV_PKGS\n\n        return _t.cast(set[str], DEV_PKGS)\n\n    from pip._internal.commands.freeze import _dev_pkgs\n\n    return _t.cast(set[str], _dev_pkgs())\n"
  },
  {
    "path": "piptools/_compat/tempfile_compat.py",
    "content": "\"\"\"\nCompatibility helpers for :mod:`tempfile` usage across platforms\nand python versions.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport collections.abc as _c\nimport contextlib\nimport os\nimport tempfile\nimport typing as _t\n\n__all__ = (\"named_temp_file\",)\n\n\n@contextlib.contextmanager\ndef named_temp_file(mode: str = \"wt\") -> _c.Iterator[_t.IO[str]]:\n    \"\"\"\n    A safe wrapper over NamedTemporaryFile for usage on Windows as well as\n    POSIX systems.\n\n    The issue we have is that we cannot guarantee that ``pip`` will open temporary\n    files on Windows with ``O_TEMPORARY`` when passed the path, resulting in surprising\n    behavior.\n    \"\"\"\n    temp_file = tempfile.NamedTemporaryFile(mode=mode, delete=False)\n    try:\n        yield temp_file\n    finally:\n        temp_file.close()\n        os.unlink(temp_file.name)\n"
  },
  {
    "path": "piptools/_internal/__init__.py",
    "content": ""
  },
  {
    "path": "piptools/_internal/_pip_api/__init__.py",
    "content": "\"\"\"\nThe ``piptools._pip_api`` subpackage defines an API layer on top of ``pip`` internals\nand usage. It is a private API for the rest of ``piptools`` to leverage.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom .cli_options import postprocess_cli_options\nfrom .install_requirements import (\n    copy_install_requirement,\n    create_install_requirement,\n    create_install_requirement_from_line,\n)\nfrom .package_finder import (\n    finder_allows_all_prereleases,\n    finder_allows_prereleases_of_req,\n)\nfrom .pip_version import (\n    PIP_VERSION,\n    PIP_VERSION_MAJOR_MINOR,\n    PIP_VERSION_TUPLE,\n    get_pip_version_for_python_executable,\n)\n\n__all__ = (\n    \"PIP_VERSION\",\n    \"PIP_VERSION_MAJOR_MINOR\",\n    \"PIP_VERSION_TUPLE\",\n    \"get_pip_version_for_python_executable\",\n    \"create_install_requirement\",\n    \"create_install_requirement_from_line\",\n    \"copy_install_requirement\",\n    \"finder_allows_all_prereleases\",\n    \"finder_allows_prereleases_of_req\",\n    \"postprocess_cli_options\",\n)\n"
  },
  {
    "path": "piptools/_internal/_pip_api/cli_options.py",
    "content": "\"\"\"\nTools for parsing pip CLI options.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport optparse\n\nfrom pip._internal.cli import cmdoptions\n\nfrom . import pip_version as _pip_version\n\n\ndef postprocess_cli_options(options: optparse.Values) -> None:\n    \"\"\"\n    After CLI parsing, pip processes options further to check various constraints and\n    coalesce values. Emulate and/or invoke those same behaviors.\n    \"\"\"\n    if _pip_version.PIP_VERSION_MAJOR_MINOR >= (26, 0):\n        cmdoptions.check_release_control_exclusive(options)\n"
  },
  {
    "path": "piptools/_internal/_pip_api/install_requirements.py",
    "content": "from __future__ import annotations\n\nimport copy\nimport typing as _t\n\nfrom pip._internal.req import InstallRequirement\nfrom pip._internal.req.constructors import install_req_from_line\nfrom pip._vendor.packaging.utils import canonicalize_name\nfrom pip._vendor.packaging.version import Version\n\nfrom . import pip_version as _pip_version\n\n\ndef create_install_requirement(\n    name: str, version: str | Version, ireq: InstallRequirement\n) -> InstallRequirement:\n    # If no extras are specified, the extras string is blank\n    extras_string = \"\"\n    extras = ireq.extras\n    if extras:\n        # Sort extras for stability\n        extras_string = f\"[{','.join(sorted(extras))}]\"\n\n    version_pin_operator = \"==\"\n    version_as_str = str(version)\n    for specifier in ireq.specifier:\n        if specifier.operator == \"===\" and specifier.version == version_as_str:\n            version_pin_operator = \"===\"\n            break\n\n    return create_install_requirement_from_line(\n        str(f\"{name}{extras_string}{version_pin_operator}{version}\"),\n        constraint=ireq.constraint,\n    )\n\n\ndef create_install_requirement_from_line(\n    *args: _t.Any, **kwargs: _t.Any\n) -> InstallRequirement:\n    return copy_install_requirement(install_req_from_line(*args, **kwargs))\n\n\ndef copy_install_requirement(\n    template: InstallRequirement, **extra_kwargs: _t.Any\n) -> InstallRequirement:\n    \"\"\"Make a copy of a template ``InstallRequirement`` with extra kwargs.\"\"\"\n    # Prepare install requirement kwargs.\n    kwargs = {\n        \"comes_from\": template.comes_from,\n        \"editable\": template.editable,\n        \"link\": template.link,\n        \"markers\": template.markers,\n        \"isolated\": template.isolated,\n        \"hash_options\": template.hash_options,\n        \"constraint\": template.constraint,\n        \"extras\": template.extras,\n        \"user_supplied\": template.user_supplied,\n    }\n    if _pip_version.PIP_VERSION_MAJOR_MINOR < (25, 3):  # pragma: <3.9 cover\n        # Ref: https://github.com/jazzband/pip-tools/issues/2252\n        kwargs[\"use_pep517\"] = template.use_pep517\n        kwargs[\"global_options\"] = template.global_options\n    kwargs.update(extra_kwargs)\n\n    if _pip_version.PIP_VERSION_MAJOR_MINOR >= (25, 3):  # pragma: >=3.9 cover\n        # Ref: https://github.com/jazzband/pip-tools/issues/2252\n        kwargs.pop(\"use_pep517\", None)\n        kwargs.pop(\"global_options\", None)\n\n    if _pip_version.PIP_VERSION_MAJOR_MINOR <= (23, 0):\n        kwargs[\"install_options\"] = template.install_options\n\n    # Original link does not belong to install requirements constructor,\n    # pop it now to update later.\n    original_link = kwargs.pop(\"original_link\", None)\n\n    # Copy template.req if not specified in extra kwargs.\n    if \"req\" not in kwargs:\n        kwargs[\"req\"] = copy.deepcopy(template.req)\n\n    kwargs[\"extras\"] = set(map(canonicalize_name, kwargs[\"extras\"]))\n    if kwargs[\"req\"]:\n        kwargs[\"req\"].extras = set(kwargs[\"extras\"])\n\n    ireq = InstallRequirement(**kwargs)\n\n    # If the original_link was None, keep it so. Passing `link` as an\n    # argument to `InstallRequirement` sets it as the original_link.\n    ireq.original_link = (\n        template.original_link if original_link is None else original_link\n    )\n\n    return ireq\n"
  },
  {
    "path": "piptools/_internal/_pip_api/package_finder.py",
    "content": "\"\"\"\nPackageFinder interfaces for pip-tools.\n\nBecause the PackageFinder class itself has evolved over pip's lifetime, these helpers\nprovide compatible interfaces which wrap methods and attributes.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom pip._internal.index.package_finder import PackageFinder\nfrom pip._internal.req import InstallRequirement\n\nfrom . import pip_version as _pip_version\n\n\ndef finder_allows_prereleases_of_req(\n    finder: PackageFinder, ireq: InstallRequirement\n) -> bool:\n    \"\"\"\n    Check if a package finder will get prereleases for a given requirement.\n\n    On older pip versions, this is not specific to the requirement, but on newer ones it\n    is.\n    \"\"\"\n    if _pip_version.PIP_VERSION_MAJOR_MINOR < (26, 0):\n        return finder.allow_all_prereleases  # type: ignore[no-any-return]\n    else:\n        return finder.release_control.allows_prereleases(  # type: ignore[no-any-return]\n            ireq.req.name\n        )\n\n\ndef finder_allows_all_prereleases(finder: PackageFinder) -> bool:\n    \"\"\"\n    Check if a package finder will get prereleases for all requirements.\n\n    On older pip versions, this is not specific to the requirement, but on newer ones it\n    is. However, ``--pre`` is translated internally to ``\":all:\"`` on those versions.\n    \"\"\"\n    if _pip_version.PIP_VERSION_MAJOR_MINOR < (26, 0):\n        return bool(finder.allow_all_prereleases)\n    else:\n        return \":all:\" in finder.release_control.all_releases\n"
  },
  {
    "path": "piptools/_internal/_pip_api/pip_version.py",
    "content": "from __future__ import annotations\n\nimport pip\nfrom pip._vendor.packaging.version import Version\nfrom pip._vendor.packaging.version import parse as parse_version\n\nfrom .. import _subprocess\n\nPIP_VERSION = parse_version(pip.__version__)\nPIP_VERSION_TUPLE: tuple[int, ...] = tuple(\n    map(int, PIP_VERSION.base_version.split(\".\"))\n)\nPIP_VERSION_MAJOR_MINOR: tuple[int, int] = PIP_VERSION_TUPLE[:2]  # type: ignore[assignment]\n\n\ndef get_pip_version_for_python_executable(python_executable: str) -> Version:\n    \"\"\"Return pip version for the given python executable.\"\"\"\n\n    str_version = _subprocess.run_python_snippet(\n        python_executable, \"import pip; print(pip.__version__)\"\n    )\n    return Version(str_version)\n"
  },
  {
    "path": "piptools/_internal/_subprocess.py",
    "content": "# WARNING! BE CAREFUL UPDATING THIS FILE\n# Consider possible security implications associated with subprocess module.\nfrom __future__ import annotations\n\nimport subprocess  # nosec\n\n\ndef run_python_snippet(python_executable: str, code_to_run: str) -> str:\n    \"\"\"\n    Execute Python code by calling ``python_executable`` with '-c' option.\n    \"\"\"\n    py_exec_cmd = python_executable, \"-c\", code_to_run\n\n    # subprocess module should never be used with untrusted input\n    return subprocess.check_output(  # nosec\n        py_exec_cmd,\n        shell=False,\n        text=True,\n    )\n"
  },
  {
    "path": "piptools/build.py",
    "content": "from __future__ import annotations\n\nimport collections\nimport contextlib\nimport os\nimport pathlib\nimport sys\nimport tempfile\nimport typing as _t\nfrom collections.abc import Iterator\nfrom dataclasses import dataclass\nfrom importlib import metadata as importlib_metadata\n\nimport build\nimport build.env\nimport pyproject_hooks\nfrom pip._internal.req import InstallRequirement\nfrom pip._internal.req.constructors import parse_req_from_line\nfrom pip._vendor.packaging.markers import Marker\nfrom pip._vendor.packaging.requirements import Requirement\n\nfrom ._compat import _tomllib_compat\nfrom ._internal import _pip_api\n\nPYPROJECT_TOML = \"pyproject.toml\"\n\n_T = _t.TypeVar(\"_T\")\n\n\nif sys.version_info >= (3, 10):\n    from importlib.metadata import PackageMetadata\nelse:\n\n    class PackageMetadata(_t.Protocol):\n        @_t.overload\n        def get_all(self, name: str, failobj: None = None) -> list[_t.Any] | None: ...\n\n        @_t.overload\n        def get_all(self, name: str, failobj: _T) -> list[_t.Any] | _T: ...\n\n\n@dataclass\nclass StaticProjectMetadata:\n    extras: tuple[str, ...]\n    requirements: tuple[InstallRequirement, ...]\n\n\n@dataclass\nclass ProjectMetadata:\n    extras: tuple[str, ...]\n    requirements: tuple[InstallRequirement, ...]\n    build_requirements: tuple[InstallRequirement, ...]\n\n\ndef maybe_statically_parse_project_metadata(\n    src_file: pathlib.Path,\n) -> StaticProjectMetadata | None:\n    \"\"\"\n    Return the metadata for a project, if it can be statically parsed from ``pyproject.toml``.\n\n    This function is typically significantly faster than invoking a build backend.\n    Returns None if the project metadata cannot be statically parsed.\n    \"\"\"\n    if src_file.name != PYPROJECT_TOML:\n        return None\n\n    try:\n        with open(src_file, \"rb\") as f:\n            pyproject_contents = _tomllib_compat.load(f)\n    except _tomllib_compat.TOMLDecodeError:\n        return None\n\n    # Not valid PEP 621 metadata\n    if (\n        \"project\" not in pyproject_contents\n        or \"name\" not in pyproject_contents[\"project\"]\n    ):\n        return None\n\n    project_table = pyproject_contents[\"project\"]\n\n    # Dynamic dependencies require build backend invocation\n    dynamic = project_table.get(\"dynamic\", [])\n    if \"dependencies\" in dynamic or \"optional-dependencies\" in dynamic:\n        return None\n\n    package_name = project_table[\"name\"]\n    comes_from = f\"{package_name} ({src_file.as_posix()})\"\n\n    extras = project_table.get(\"optional-dependencies\", {}).keys()\n    install_requirements = [\n        InstallRequirement(Requirement(req), comes_from)\n        for req in project_table.get(\"dependencies\", [])\n    ]\n    for extra, reqs in (\n        pyproject_contents.get(\"project\", {}).get(\"optional-dependencies\", {}).items()\n    ):\n        for req in reqs:\n            requirement = Requirement(req)\n            if requirement.name == package_name:\n                # Similar to logic for handling self-referential requirements\n                # from _prepare_requirements\n                requirement.url = src_file.parent.absolute().as_uri()\n\n            # Note we don't need to modify `requirement` to include this extra\n            marker = Marker(f\"extra == '{extra}'\")\n            install_requirements.append(\n                InstallRequirement(requirement, comes_from, markers=marker)\n            )\n\n    return StaticProjectMetadata(\n        extras=tuple(extras),\n        requirements=tuple(install_requirements),\n    )\n\n\ndef build_project_metadata(\n    src_file: pathlib.Path,\n    build_targets: tuple[str, ...],\n    *,\n    upgrade_packages: tuple[str, ...] | None = None,\n    attempt_static_parse: bool,\n    isolated: bool,\n    quiet: bool,\n) -> ProjectMetadata | StaticProjectMetadata:\n    \"\"\"\n    Return the metadata for a project.\n\n    First, optionally attempt to determine the metadata statically from the\n    ``pyproject.toml`` file. This will not work if build_targets are specified,\n    since we cannot determine build requirements statically.\n\n    Uses the ``prepare_metadata_for_build_wheel`` hook for the wheel metadata\n    if available, otherwise ``build_wheel``.\n\n    Uses the ``prepare_metadata_for_build_{target}`` hook for each ``build_targets``\n    if available.\n\n    :param src_file: Project source file\n    :param build_targets: A tuple of build targets to get the dependencies\n                                of (``sdist`` or ``wheel`` or ``editable``).\n    :param attempt_static_parse: Whether to attempt to statically parse the\n                                 project metadata from ``pyproject.toml``.\n                                 Cannot be used with ``build_targets``.\n    :param isolated: Whether to run invoke the backend in the current\n                     environment or to create an isolated one and invoke it\n                     there.\n    :param quiet: Whether to suppress the output of subprocesses.\n    \"\"\"\n\n    if attempt_static_parse:\n        if build_targets:\n            raise ValueError(\n                \"Cannot execute the PEP 517 optional get_requires_for_build* \"\n                \"hooks statically, as build requirements are requested\"\n            )\n        project_metadata = maybe_statically_parse_project_metadata(src_file)\n        if project_metadata is not None:\n            return project_metadata\n\n    src_dir = src_file.parent\n    with _create_project_builder(\n        src_dir,\n        upgrade_packages=upgrade_packages,\n        isolated=isolated,\n        quiet=quiet,\n    ) as builder:\n        metadata = _build_project_wheel_metadata(builder)\n        extras = tuple(metadata.get_all(\"Provides-Extra\") or ())\n        requirements = tuple(\n            _prepare_requirements(metadata=metadata, src_file=src_file)\n        )\n        build_requirements = tuple(\n            _prepare_build_requirements(\n                builder=builder,\n                src_file=src_file,\n                build_targets=build_targets,\n                package_name=_get_name(metadata),\n            )\n        )\n        return ProjectMetadata(\n            extras=extras,\n            requirements=requirements,\n            build_requirements=build_requirements,\n        )\n\n\n@contextlib.contextmanager\ndef _env_var(\n    env_var_name: str,\n    env_var_value: str,\n    /,\n) -> Iterator[None]:\n    sentinel = object()\n    original_pip_constraint = os.getenv(env_var_name, sentinel)\n    pip_constraint_was_unset = original_pip_constraint is sentinel\n\n    os.environ[env_var_name] = env_var_value\n    try:\n        yield\n    finally:\n        if pip_constraint_was_unset:\n            del os.environ[env_var_name]\n        else:\n            # Assert here is necessary because MyPy can't infer type\n            # narrowing in the complex case.\n            assert isinstance(original_pip_constraint, str)\n            os.environ[env_var_name] = original_pip_constraint\n\n\n@contextlib.contextmanager\ndef _temporary_constraints_file_set_for_pip(\n    upgrade_packages: tuple[str, ...],\n) -> Iterator[None]:\n    with tempfile.NamedTemporaryFile(\n        mode=\"w+t\",\n        delete=False,  # FIXME: switch to `delete_on_close` in Python 3.12+\n    ) as tmpfile:\n        # NOTE: `delete_on_close=False` here (or rather `delete=False`,\n        # NOTE: temporarily) is important for cross-platform execution. It is\n        # NOTE: required on Windows so that the underlying `pip install`\n        # NOTE: invocation by pypa/build will be able to access the constraint\n        # NOTE: file via a subprocess and not fail installing it due to a\n        # NOTE: permission error related to this file handle still open in our\n        # NOTE: parent process. To achieve this, we `.close()` the file\n        # NOTE: descriptor before we hand off the control to the build frontend\n        # NOTE: and with `delete_on_close=False`, the\n        # NOTE: `tempfile.NamedTemporaryFile()` context manager does not remove\n        # NOTE: it from disk right away.\n        # NOTE: Due to support of versions below Python 3.12, we are forced to\n        # NOTE: temporarily resort to using `delete=False`, meaning that the CM\n        # NOTE: never attempts removing the file from disk, not even on exit.\n        # NOTE: So we do this manually until we can migrate to using the more\n        # NOTE: ergonomic argument `delete_on_close=False`.\n\n        # Write packages to upgrade to a temporary file to set as\n        # constraints for the installation to the builder environment,\n        # in case build requirements are among them\n        tmpfile.write(\"\\n\".join(upgrade_packages))\n\n        # FIXME: replace `delete` with `delete_on_close` in Python 3.12+\n        # FIXME: and replace `.close()` with `.flush()`\n        tmpfile.close()\n\n        try:\n            with _env_var(\"PIP_CONSTRAINT\", tmpfile.name):\n                yield\n        finally:\n            # FIXME: replace `delete` with `delete_on_close` in Python 3.12+\n            # FIXME: and drop this manual deletion\n            os.unlink(tmpfile.name)\n\n\n@contextlib.contextmanager\ndef _create_project_builder(\n    src_dir: pathlib.Path,\n    *,\n    upgrade_packages: tuple[str, ...] | None = None,\n    isolated: bool,\n    quiet: bool,\n) -> Iterator[build.ProjectBuilder]:\n    if quiet:\n        runner = pyproject_hooks.quiet_subprocess_runner\n    else:\n        runner = pyproject_hooks.default_subprocess_runner\n\n    if not isolated:\n        yield build.ProjectBuilder(src_dir, runner=runner)\n        return\n\n    maybe_pip_constrained_context = (\n        contextlib.nullcontext()\n        if upgrade_packages is None\n        else _temporary_constraints_file_set_for_pip(upgrade_packages)\n    )\n\n    with maybe_pip_constrained_context, build.env.DefaultIsolatedEnv() as env:\n        builder = build.ProjectBuilder.from_isolated_env(env, src_dir, runner)\n        env.install(builder.build_system_requires)\n        env.install(builder.get_requires_for_build(\"wheel\"))\n        yield builder\n\n\ndef _build_project_wheel_metadata(\n    builder: build.ProjectBuilder,\n) -> PackageMetadata:\n    with tempfile.TemporaryDirectory() as tmpdir:\n        path = pathlib.Path(builder.metadata_path(tmpdir))\n        return importlib_metadata.PathDistribution(path).metadata\n\n\ndef _get_name(metadata: PackageMetadata) -> str:\n    retval = metadata.get_all(\"Name\")[0]  # type: ignore[index]\n    assert isinstance(retval, str)\n    return retval\n\n\ndef _prepare_requirements(\n    metadata: PackageMetadata, src_file: pathlib.Path\n) -> Iterator[InstallRequirement]:\n    package_name = _get_name(metadata)\n    comes_from = f\"{package_name} ({src_file.as_posix()})\"\n    package_dir = src_file.parent\n\n    for req in metadata.get_all(\"Requires-Dist\") or []:\n        parts = parse_req_from_line(req, comes_from)\n        if parts.requirement.name == package_name:\n            # Replace package name with package directory in the requirement\n            # string so that pip can find the package as self-referential.\n            # Note the string can contain extras, so we need to replace only\n            # the package name, not the whole string.\n            replaced_package_name = req.replace(package_name, str(package_dir), 1)\n            parts = parse_req_from_line(replaced_package_name, comes_from)\n\n        yield _pip_api.copy_install_requirement(\n            InstallRequirement(\n                parts.requirement,\n                comes_from,\n                link=parts.link,\n                markers=parts.markers,\n                extras=parts.extras,\n            )\n        )\n\n\ndef _prepare_build_requirements(\n    builder: build.ProjectBuilder,\n    src_file: pathlib.Path,\n    build_targets: tuple[str, ...],\n    package_name: str,\n) -> Iterator[InstallRequirement]:\n    result = collections.defaultdict(set)\n\n    # Build requirements will only be present if a pyproject.toml file exists,\n    # but if there is also a setup.py file then only that will be explicitly\n    # processed due to the order of `DEFAULT_REQUIREMENTS_FILES`.\n    src_file = src_file.parent / PYPROJECT_TOML\n\n    for req in builder.build_system_requires:\n        result[req].add(f\"{package_name} ({src_file}::build-system.requires)\")\n    for build_target in build_targets:\n        for req in builder.get_requires_for_build(build_target):\n            result[req].add(\n                f\"{package_name} ({src_file}::build-system.backend::{build_target})\"\n            )\n\n    for req, comes_from_sources in result.items():\n        for comes_from in comes_from_sources:\n            yield _pip_api.create_install_requirement_from_line(\n                req, comes_from=comes_from\n            )\n"
  },
  {
    "path": "piptools/cache.py",
    "content": "from __future__ import annotations\n\nimport json\nimport os\nimport platform\nimport sys\nimport typing as _t\nfrom collections.abc import Iterable\n\nfrom pip._internal.req import InstallRequirement\nfrom pip._vendor.packaging.requirements import Requirement\n\nfrom .exceptions import PipToolsError\nfrom .utils import as_tuple, key_from_req, lookup_table_from_tuples\n\nCacheKey = tuple[str, str]\nCacheLookup = dict[str, list[str]]\nCacheDict = dict[str, CacheLookup]\n\n_PEP425_PY_TAGS = {\"cpython\": \"cp\", \"pypy\": \"pp\", \"ironpython\": \"ip\", \"jython\": \"jy\"}\n\n\ndef _implementation_name() -> str:\n    \"\"\"\n    Get Python implementation and version.\n\n    Similar to PEP 425, however the minor version is separated from the major to\n    differentiate \"3.10\" and \"31.0\".\n    \"\"\"\n    implementation_name = platform.python_implementation().lower()\n    implementation = _PEP425_PY_TAGS.get(implementation_name, \"??\")\n    return \"{}{}.{}\".format(implementation, *sys.version_info)\n\n\nclass CorruptCacheError(PipToolsError):\n    def __init__(self, path: str):\n        self.path = path\n\n    def __str__(self) -> str:\n        lines = [\n            \"The dependency cache seems to have been corrupted.\",\n            \"Inspect, or delete, the following file:\",\n            f\"  {self.path}\",\n        ]\n        return os.linesep.join(lines)\n\n\ndef read_cache_file(cache_file_path: str) -> CacheDict:\n    with open(cache_file_path, encoding=\"utf-8\") as cache_file:\n        try:\n            doc = json.load(cache_file)\n        except (json.JSONDecodeError, UnicodeDecodeError):\n            raise CorruptCacheError(cache_file_path)\n\n        # Check version and load the contents\n        if doc[\"__format__\"] != 1:\n            raise ValueError(\"Unknown cache file format\")\n        return _t.cast(CacheDict, doc[\"dependencies\"])\n\n\nclass DependencyCache:\n    \"\"\"\n    Create new persistent dependency cache for the current Python version.\n\n    The cache file is written to the appropriate user cache dir for the\n    current platform, i.e.\n\n        ~/.cache/pip-tools/depcache-pyX.Y.json\n\n    Where py indicates the Python implementation.\n    Where X.Y indicates the Python version.\n    \"\"\"\n\n    def __init__(self, cache_dir: str):\n        os.makedirs(cache_dir, exist_ok=True)\n        cache_filename = f\"depcache-{_implementation_name()}.json\"\n\n        self._cache_file = os.path.join(cache_dir, cache_filename)\n        self._cache: CacheDict | None = None\n\n    @property\n    def cache(self) -> CacheDict:\n        \"\"\"\n        The dictionary that is the actual in-memory cache.  This property\n        lazily loads the cache from disk.\n        \"\"\"\n        if self._cache is None:\n            try:\n                self._cache = read_cache_file(self._cache_file)\n            except FileNotFoundError:\n                self._cache = {}\n        return self._cache\n\n    def as_cache_key(self, ireq: InstallRequirement) -> CacheKey:\n        \"\"\"\n        Given a requirement, return its cache key.\n\n        This behavior is a little weird\n        in order to allow backwards compatibility with cache files. For a requirement\n        without extras, this will return, for example:\n\n        (\"ipython\", \"2.1.0\")\n\n        For a requirement with extras, the extras will be comma-separated and appended\n        to the version, inside brackets, like so:\n\n        (\"ipython\", \"2.1.0[nbconvert,notebook]\")\n        \"\"\"\n        name, version, extras = as_tuple(ireq)\n        if not extras:\n            extras_string = \"\"\n        else:\n            extras_string = f\"[{','.join(extras)}]\"\n        return name, f\"{version}{extras_string}\"\n\n    def write_cache(self) -> None:\n        \"\"\"Write the cache to disk as JSON.\"\"\"\n        doc = {\"__format__\": 1, \"dependencies\": self._cache}\n        with open(self._cache_file, \"w\", encoding=\"utf-8\") as f:\n            json.dump(doc, f, sort_keys=True)\n\n    def clear(self) -> None:\n        self._cache = {}\n        self.write_cache()\n\n    def __contains__(self, ireq: InstallRequirement) -> bool:\n        pkgname, pkgversion_and_extras = self.as_cache_key(ireq)\n        return pkgversion_and_extras in self.cache.get(pkgname, {})\n\n    def __getitem__(self, ireq: InstallRequirement) -> list[str]:\n        pkgname, pkgversion_and_extras = self.as_cache_key(ireq)\n        return self.cache[pkgname][pkgversion_and_extras]\n\n    def __setitem__(self, ireq: InstallRequirement, values: list[str]) -> None:\n        pkgname, pkgversion_and_extras = self.as_cache_key(ireq)\n        self.cache.setdefault(pkgname, {})\n        self.cache[pkgname][pkgversion_and_extras] = values\n        self.write_cache()\n\n    def reverse_dependencies(\n        self, ireqs: Iterable[InstallRequirement]\n    ) -> dict[str, set[str]]:\n        \"\"\"\n        Return a lookup table of reverse dependencies for all the given ireqs.\n\n        Since this is all static, it only works if the dependency cache\n        contains the complete data, otherwise you end up with a partial view.\n        This is typically no problem if you use this function after the entire\n        dependency tree is resolved.\n        \"\"\"\n        ireqs_as_cache_values = [self.as_cache_key(ireq) for ireq in ireqs]\n        return self._reverse_dependencies(ireqs_as_cache_values)\n\n    def _reverse_dependencies(\n        self, cache_keys: Iterable[tuple[str, str]]\n    ) -> dict[str, set[str]]:\n        \"\"\"\n        Return a lookup table of reverse dependencies for all the given cache keys.\n\n        Example input:\n\n            [('pep8', '1.5.7'),\n             ('flake8', '2.4.0'),\n             ('mccabe', '0.3'),\n             ('pyflakes', '0.8.1')]\n\n        Example output:\n\n            {'pep8': ['flake8'],\n             'flake8': [],\n             'mccabe': ['flake8'],\n             'pyflakes': ['flake8']}\n\n        \"\"\"\n        # First, collect all the dependencies into a sequence of (parent, child)\n        # tuples, like [('flake8', 'pep8'), ('flake8', 'mccabe'), ...]\n        return lookup_table_from_tuples(\n            (key_from_req(Requirement(dep_name)), name)\n            for name, version_and_extras in cache_keys\n            for dep_name in self.cache[name][version_and_extras]\n        )\n"
  },
  {
    "path": "piptools/exceptions.py",
    "content": "from __future__ import annotations\n\nimport operator\nfrom collections.abc import Iterable\n\nfrom pip._internal.index.package_finder import PackageFinder\nfrom pip._internal.models.candidate import InstallationCandidate\nfrom pip._internal.req import InstallRequirement\nfrom pip._internal.utils.misc import redact_auth_from_url\n\nfrom ._internal import _pip_api\n\n\nclass PipToolsError(Exception):\n    pass\n\n\nclass NoCandidateFound(PipToolsError):\n    def __init__(\n        self,\n        ireq: InstallRequirement,\n        candidates_tried: Iterable[InstallationCandidate],\n        finder: PackageFinder,\n    ) -> None:\n        self.ireq = ireq\n        self.candidates_tried = candidates_tried\n        self.finder = finder\n\n    def __str__(self) -> str:\n        versions = []\n        pre_versions = []\n\n        for candidate in sorted(\n            self.candidates_tried, key=operator.attrgetter(\"version\")\n        ):\n            version = str(candidate.version)\n            if candidate.version.is_prerelease:\n                pre_versions.append(version)\n            else:\n                versions.append(version)\n\n        lines = [f\"Could not find a version that matches {self.ireq}\"]\n\n        if versions:\n            lines.append(f\"Tried: {', '.join(versions)}\")\n\n        if pre_versions:\n            if _pip_api.finder_allows_prereleases_of_req(self.finder, self.ireq):\n                line = \"Tried\"\n            else:\n                line = \"Skipped\"\n\n            line += f\" pre-versions: {', '.join(pre_versions)}\"\n            lines.append(line)\n\n        if versions or pre_versions:\n            lines.append(\n                \"There are incompatible versions in the resolved dependencies:\"\n            )\n            source_ireqs = getattr(self.ireq, \"_source_ireqs\", [])\n            lines.extend(f\"  {ireq}\" for ireq in source_ireqs)\n        else:\n            redacted_urls = tuple(\n                redact_auth_from_url(url) for url in self.finder.index_urls\n            )\n            lines.append(\"No versions found\")\n            lines.append(\n                \"{} {} reachable?\".format(\n                    \"Were\" if len(redacted_urls) > 1 else \"Was\",\n                    \" or \".join(redacted_urls),\n                )\n            )\n        return \"\\n\".join(lines)\n\n\nclass IncompatibleRequirements(PipToolsError):\n    def __init__(self, ireq_a: InstallRequirement, ireq_b: InstallRequirement) -> None:\n        self.ireq_a = ireq_a\n        self.ireq_b = ireq_b\n\n    def __str__(self) -> str:\n        message = \"Incompatible requirements found: {} and {}\"\n        return message.format(self.ireq_a, self.ireq_b)\n"
  },
  {
    "path": "piptools/locations.py",
    "content": "from __future__ import annotations\n\nfrom pip._internal.utils.appdirs import user_cache_dir\n\n# The user_cache_dir helper comes straight from pip itself\nCACHE_DIR = user_cache_dir(\"pip-tools\")\n\n# The project defaults specific to pip-tools should be written to this filenames\nDEFAULT_CONFIG_FILE_NAMES = (\".pip-tools.toml\", \"pyproject.toml\")\n"
  },
  {
    "path": "piptools/logging.py",
    "content": "from __future__ import annotations\n\nimport contextlib\nimport logging\nimport sys\nimport typing as _t\nfrom collections.abc import Iterator\n\nimport click\n\n# Initialise the builtin logging module for other component using it.\n# Ex: pip\nlogging.basicConfig()\n\n\nclass LogContext:\n    stream = sys.stderr\n\n    def __init__(self, verbosity: int = 0, indent_width: int = 2):\n        self.verbosity = self._initial_verbosity = verbosity\n        self.current_indent = self._initial_indent = 0\n        self._indent_width = self._initial_indent_width = indent_width\n\n    def log(self, message: str, *args: _t.Any, **kwargs: _t.Any) -> None:\n        kwargs.setdefault(\"err\", True)\n        prefix = \" \" * self.current_indent\n        click.secho(prefix + message, *args, **kwargs)\n\n    def debug(self, message: str, *args: _t.Any, **kwargs: _t.Any) -> None:\n        if self.verbosity >= 1:\n            self.log(message, *args, **kwargs)\n\n    def info(self, message: str, *args: _t.Any, **kwargs: _t.Any) -> None:\n        if self.verbosity >= 0:\n            self.log(message, *args, **kwargs)\n\n    def warning(self, message: str, *args: _t.Any, **kwargs: _t.Any) -> None:\n        kwargs.setdefault(\"fg\", \"yellow\")\n        self.log(message, *args, **kwargs)\n\n    def error(self, message: str, *args: _t.Any, **kwargs: _t.Any) -> None:\n        kwargs.setdefault(\"fg\", \"red\")\n        self.log(message, *args, **kwargs)\n\n    def _indent(self) -> None:\n        self.current_indent += self._indent_width\n\n    def _dedent(self) -> None:\n        self.current_indent -= self._indent_width\n\n    @contextlib.contextmanager\n    def indentation(self) -> Iterator[None]:\n        \"\"\"\n        Increase indentation.\n        \"\"\"\n        self._indent()\n        try:\n            yield\n        finally:\n            self._dedent()\n\n    def reset(self) -> None:\n        \"\"\"Reset logger to initial state.\"\"\"\n        self.verbosity = self._initial_verbosity\n        self.current_indent = self._initial_indent\n        self._indent_width = self._initial_indent_width\n\n\nlog = LogContext()\n"
  },
  {
    "path": "piptools/py.typed",
    "content": ""
  },
  {
    "path": "piptools/repositories/__init__.py",
    "content": "from __future__ import annotations\n\nfrom .local import LocalRequirementsRepository\nfrom .pypi import PyPIRepository\n\n__all__ = [\"LocalRequirementsRepository\", \"PyPIRepository\"]\n"
  },
  {
    "path": "piptools/repositories/base.py",
    "content": "from __future__ import annotations\n\nimport optparse\nfrom abc import ABCMeta, abstractmethod\nfrom collections.abc import Iterator\nfrom contextlib import contextmanager\n\nfrom pip._internal.commands.install import InstallCommand\nfrom pip._internal.index.package_finder import PackageFinder\nfrom pip._internal.models.index import PyPI\nfrom pip._internal.network.session import PipSession\nfrom pip._internal.req import InstallRequirement\n\n\nclass BaseRepository(metaclass=ABCMeta):\n    DEFAULT_INDEX_URL = PyPI.simple_url\n\n    def clear_caches(self) -> None:\n        \"\"\"Should clear any caches used by the implementation.\"\"\"\n\n    @abstractmethod\n    def find_best_match(\n        self, ireq: InstallRequirement, prereleases: bool | None\n    ) -> InstallRequirement:\n        \"\"\"\n        Returns a pinned InstallRequirement object that indicates the best match\n        for the given InstallRequirement according to the external repository.\n        \"\"\"\n\n    @abstractmethod\n    def get_dependencies(self, ireq: InstallRequirement) -> set[InstallRequirement]:\n        \"\"\"\n        Given a pinned, URL, or editable InstallRequirement, returns a set of\n        dependencies (also InstallRequirements, but not necessarily pinned).\n        They indicate the secondary dependencies for the given requirement.\n        \"\"\"\n\n    @abstractmethod\n    def get_hashes(self, ireq: InstallRequirement) -> set[str]:\n        \"\"\"\n        Given a pinned InstallRequirement, returns a set of hashes that represent\n        all of the files for a given requirement. It is not acceptable for an\n        editable or unpinned requirement to be passed to this function.\n        \"\"\"\n\n    @abstractmethod\n    @contextmanager\n    def allow_all_wheels(self) -> Iterator[None]:\n        \"\"\"\n        Monkey patches pip.Wheel to allow wheels from all platforms and Python versions.\n        \"\"\"\n\n    @property\n    @abstractmethod\n    def options(self) -> optparse.Values:\n        \"\"\"Returns parsed pip options\"\"\"\n\n    @property\n    @abstractmethod\n    def session(self) -> PipSession:\n        \"\"\"Returns a session to make requests\"\"\"\n\n    @property\n    @abstractmethod\n    def finder(self) -> PackageFinder:\n        \"\"\"Returns a package finder to interact with simple repository API (PEP 503)\"\"\"\n\n    @property\n    @abstractmethod\n    def command(self) -> InstallCommand:\n        \"\"\"Return an install command.\"\"\"\n"
  },
  {
    "path": "piptools/repositories/local.py",
    "content": "from __future__ import annotations\n\nimport optparse\nimport typing as _t\nfrom collections.abc import Iterator, Mapping\nfrom contextlib import contextmanager\n\nfrom pip._internal.commands.install import InstallCommand\nfrom pip._internal.index.package_finder import PackageFinder\nfrom pip._internal.models.candidate import InstallationCandidate\nfrom pip._internal.network.session import PipSession\nfrom pip._internal.req import InstallRequirement\nfrom pip._internal.utils.hashes import FAVORITE_HASH\n\nfrom .._internal import _pip_api\nfrom ..utils import as_tuple, key_from_ireq\nfrom .base import BaseRepository\nfrom .pypi import PyPIRepository\n\n\ndef ireq_satisfied_by_existing_pin(\n    ireq: InstallRequirement, existing_pin: InstallationCandidate\n) -> bool:\n    \"\"\"\n    Return :py:data:`True` if the given ``InstallRequirement`` is satisfied by the\n    previously encountered version pin.\n    \"\"\"\n    version = next(iter(existing_pin.req.specifier)).version\n    result = ireq.req.specifier.contains(\n        version, prereleases=existing_pin.req.specifier.prereleases\n    )\n    return _t.cast(bool, result)\n\n\nclass LocalRequirementsRepository(BaseRepository):\n    \"\"\"\n    The LocalRequirementsRepository proxied the _real_ repository by first\n    checking if a requirement can be satisfied by existing pins (i.e. the\n    result of a previous compile step).\n\n    In effect, if a requirement can be satisfied with a version pinned in the\n    requirements file, we prefer that version over the best match found in\n    PyPI.  This keeps updates to the requirements.txt down to a minimum.\n    \"\"\"\n\n    def __init__(\n        self,\n        existing_pins: Mapping[str, InstallationCandidate],\n        proxied_repository: PyPIRepository,\n        reuse_hashes: bool = True,\n    ):\n        self._reuse_hashes = reuse_hashes\n        self.repository = proxied_repository\n        self.existing_pins = existing_pins\n\n    @property\n    def options(self) -> optparse.Values:\n        return self.repository.options\n\n    @property\n    def finder(self) -> PackageFinder:\n        return self.repository.finder\n\n    @property\n    def session(self) -> PipSession:\n        return self.repository.session\n\n    @property\n    def command(self) -> InstallCommand:\n        \"\"\"Return an install command instance.\"\"\"\n        return self.repository.command\n\n    def clear_caches(self) -> None:\n        self.repository.clear_caches()\n\n    def find_best_match(\n        self, ireq: InstallRequirement, prereleases: bool | None = None\n    ) -> InstallationCandidate:\n        key = key_from_ireq(ireq)\n        existing_pin = self.existing_pins.get(key)\n        if existing_pin and ireq_satisfied_by_existing_pin(ireq, existing_pin):\n            project, version, _ = as_tuple(existing_pin)\n            return _pip_api.create_install_requirement(project, version, ireq)\n        else:\n            return self.repository.find_best_match(ireq, prereleases)\n\n    def get_dependencies(self, ireq: InstallRequirement) -> set[InstallRequirement]:\n        return self.repository.get_dependencies(ireq)\n\n    def get_hashes(self, ireq: InstallRequirement) -> set[str]:\n        existing_pin = self._reuse_hashes and self.existing_pins.get(\n            key_from_ireq(ireq)\n        )\n        if existing_pin and ireq_satisfied_by_existing_pin(ireq, existing_pin):\n            hashes = existing_pin.hash_options\n            hexdigests = hashes.get(FAVORITE_HASH)\n            if hexdigests:\n                return {\n                    \":\".join([FAVORITE_HASH, hexdigest]) for hexdigest in hexdigests\n                }\n        return self.repository.get_hashes(ireq)\n\n    @contextmanager\n    def allow_all_wheels(self) -> Iterator[None]:\n        with self.repository.allow_all_wheels():\n            yield\n"
  },
  {
    "path": "piptools/repositories/pypi.py",
    "content": "from __future__ import annotations\n\nimport contextlib\nimport hashlib\nimport itertools\nimport optparse\nimport os\nimport typing as _t\nfrom collections.abc import Iterator\nfrom contextlib import contextmanager\nfrom shutil import rmtree\n\nfrom click import progressbar\nfrom pip._internal.cache import WheelCache\nfrom pip._internal.commands import create_command\nfrom pip._internal.commands.install import InstallCommand\nfrom pip._internal.index.package_finder import PackageFinder\nfrom pip._internal.models.candidate import InstallationCandidate\nfrom pip._internal.models.index import PackageIndex\nfrom pip._internal.models.link import Link\nfrom pip._internal.models.wheel import Wheel\nfrom pip._internal.network.session import PipSession\nfrom pip._internal.operations.build.build_tracker import get_build_tracker\nfrom pip._internal.req import InstallRequirement, RequirementSet\nfrom pip._internal.utils.hashes import FAVORITE_HASH\nfrom pip._internal.utils.logging import indent_log, setup_logging\nfrom pip._internal.utils.misc import normalize_path\nfrom pip._internal.utils.temp_dir import TempDirectory, global_tempdir_manager\nfrom pip._internal.utils.urls import path_to_url, url_to_path\nfrom pip._vendor.packaging.tags import Tag\nfrom pip._vendor.packaging.version import _BaseVersion\nfrom pip._vendor.requests import RequestException, Session\n\nfrom .._compat import create_wheel_cache\nfrom .._internal import _pip_api\nfrom ..exceptions import NoCandidateFound\nfrom ..logging import log\nfrom ..utils import (\n    as_tuple,\n    is_pinned_requirement,\n    is_url_requirement,\n    lookup_table,\n)\nfrom .base import BaseRepository\n\nFILE_CHUNK_SIZE = 4096\n\n\nclass FileStream(_t.NamedTuple):\n    stream: _t.BinaryIO\n    size: float | None\n\n\nclass PyPIRepository(BaseRepository):\n    HASHABLE_PACKAGE_TYPES = {\"bdist_wheel\", \"sdist\"}\n\n    \"\"\"\n    The PyPIRepository will use the provided Finder instance to lookup\n    packages.  Typically, it looks up packages on PyPI (the default implicit\n    config), but any other PyPI mirror can be used if index_urls is\n    changed/configured on the Finder.\n    \"\"\"\n\n    def __init__(self, pip_args: list[str], cache_dir: str):\n        # Use pip's parser for pip.conf management and defaults.\n        # General options (find_links, index_url, extra_index_url, trusted_host,\n        # and pre) are deferred to pip.\n        self._command: InstallCommand = create_command(\"install\")\n\n        options, _ = self.command.parse_args(pip_args)\n        _pip_api.postprocess_cli_options(options)\n\n        if options.cache_dir:\n            options.cache_dir = normalize_path(options.cache_dir)\n        options.require_hashes = False\n        options.ignore_dependencies = False\n\n        self._options: optparse.Values = options\n        self._session = self.command._build_session(options)\n        self._finder = self.command._build_package_finder(\n            options=options, session=self.session\n        )\n\n        # Caches\n        # stores project_name => InstallationCandidate mappings for all\n        # versions reported by PyPI, so we only have to ask once for each\n        # project\n        self._available_candidates_cache: dict[str, list[InstallationCandidate]] = {}\n\n        # stores InstallRequirement => list(InstallRequirement) mappings\n        # of all secondary dependencies for the given requirement, so we\n        # only have to go to disk once for each requirement\n        self._dependencies_cache: dict[InstallRequirement, set[InstallRequirement]] = {}\n\n        # Setup file paths\n        self._cache_dir = normalize_path(str(cache_dir))\n        self._download_dir = os.path.join(self._cache_dir, \"pkgs\")\n\n        # Default pip's logger is noisy, so decrease it's verbosity\n        setup_logging(\n            verbosity=log.verbosity - 1,\n            no_color=self.options.no_color,\n            user_log_file=self.options.log,\n        )\n\n    def clear_caches(self) -> None:\n        rmtree(self._download_dir, ignore_errors=True)\n\n    @property\n    def options(self) -> optparse.Values:\n        return self._options\n\n    @property\n    def session(self) -> PipSession:\n        return self._session\n\n    @property\n    def finder(self) -> PackageFinder:\n        return self._finder\n\n    def _clear_finder_cache(self) -> None:\n        \"\"\"Clear the cache of installation candidates.\"\"\"\n        # `finder.find_all_candidates` is an lru_cache wrapped method on older `pip`\n        # versions, which can be cleared with `cache_clear()`\n        # but on newer versions, it's a simple method, and the underlying cache is a\n        # dict on the instance\n        # the same holds for `finder.find_best_candidate`\n        if _pip_api.PIP_VERSION_MAJOR_MINOR >= (25, 1):\n            self.finder._all_candidates.clear()\n            self.finder._best_candidates.clear()\n        else:\n            self.finder.find_all_candidates.cache_clear()\n            self.finder.find_best_candidate.cache_clear()\n\n    @property\n    def command(self) -> InstallCommand:\n        \"\"\"Return an install command instance.\"\"\"\n        return self._command\n\n    def find_all_candidates(self, req_name: str) -> list[InstallationCandidate]:\n        if req_name not in self._available_candidates_cache:\n            candidates = self.finder.find_all_candidates(req_name)\n            self._available_candidates_cache[req_name] = candidates\n        return self._available_candidates_cache[req_name]\n\n    def find_best_match(\n        self, ireq: InstallRequirement, prereleases: bool | None = None\n    ) -> InstallRequirement:\n        \"\"\"\n        Returns a pinned InstallRequirement object that indicates the best match\n        for the given InstallRequirement according to the external repository.\n        \"\"\"\n        if ireq.editable or is_url_requirement(ireq):\n            return ireq  # return itself as the best match\n\n        all_candidates = self.find_all_candidates(ireq.name)\n        candidates_by_version = lookup_table(all_candidates, key=candidate_version)\n        matching_versions = ireq.specifier.filter(\n            (candidate.version for candidate in all_candidates), prereleases=prereleases\n        )\n\n        matching_candidates = list(\n            itertools.chain.from_iterable(\n                candidates_by_version[ver] for ver in matching_versions\n            )\n        )\n        if not matching_candidates:\n            raise NoCandidateFound(ireq, all_candidates, self.finder)\n\n        evaluator = self.finder.make_candidate_evaluator(ireq.name)\n        best_candidate_result = evaluator.compute_best_candidate(matching_candidates)\n        best_candidate = best_candidate_result.best_candidate\n\n        # Turn the candidate into a pinned InstallRequirement\n        return _pip_api.create_install_requirement(\n            best_candidate.name,\n            best_candidate.version,\n            ireq,\n        )\n\n    def resolve_reqs(\n        self,\n        download_dir: str | None,\n        ireq: InstallRequirement,\n        wheel_cache: WheelCache,\n    ) -> set[InstallationCandidate]:\n        with (\n            get_build_tracker() as build_tracker,\n            TempDirectory(kind=\"resolver\") as temp_dir,\n            indent_log(),\n        ):\n            preparer_kwargs = {\n                \"temp_build_dir\": temp_dir,\n                \"options\": self.options,\n                \"session\": self.session,\n                \"finder\": self.finder,\n                \"use_user_site\": False,\n                \"download_dir\": download_dir,\n                \"build_tracker\": build_tracker,\n            }\n            preparer = self.command.make_requirement_preparer(**preparer_kwargs)\n\n            reqset = RequirementSet()\n            ireq.user_supplied = True\n            if getattr(ireq, \"name\", None):\n                reqset.add_named_requirement(ireq)\n            else:\n                reqset.add_unnamed_requirement(ireq)\n\n            resolver = self.command.make_resolver(\n                preparer=preparer,\n                finder=self.finder,\n                options=self.options,\n                wheel_cache=wheel_cache,\n                use_user_site=False,\n                ignore_installed=True,\n                ignore_requires_python=False,\n                force_reinstall=False,\n                upgrade_strategy=\"to-satisfy-only\",\n            )\n            results = resolver._resolve_one(reqset, ireq)\n            if not ireq.prepared:\n                # If still not prepared, e.g. a constraint, do enough to assign\n                # the ireq a name:\n                resolver._get_dist_for(ireq)\n\n        return set(results)\n\n    def get_dependencies(self, ireq: InstallRequirement) -> set[InstallRequirement]:\n        \"\"\"\n        Given a pinned, URL, or editable InstallRequirement, returns a set of\n        dependencies (also InstallRequirements, but not necessarily pinned).\n        They indicate the secondary dependencies for the given requirement.\n        \"\"\"\n        if not (\n            ireq.editable or is_url_requirement(ireq) or is_pinned_requirement(ireq)\n        ):\n            raise TypeError(\n                f\"Expected url, pinned or editable InstallRequirement, got {ireq}\"\n            )\n\n        if ireq not in self._dependencies_cache:\n            if ireq.editable and (ireq.source_dir and os.path.exists(ireq.source_dir)):\n                # No download_dir for locally available editable requirements.\n                # If a download_dir is passed, pip will unnecessarily archive\n                # the entire source directory\n                download_dir = None\n            elif ireq.link and ireq.link.is_vcs:\n                # No download_dir for VCS sources.  This also works around pip\n                # using git-checkout-index, which gets rid of the .git dir.\n                download_dir = None\n            else:\n                download_dir = self._get_download_path(ireq)\n                os.makedirs(download_dir, exist_ok=True)\n\n            with global_tempdir_manager():\n                wheel_cache = create_wheel_cache(\n                    cache_dir=self._cache_dir,\n                    format_control=self.options.format_control,\n                )\n                self._dependencies_cache[ireq] = self.resolve_reqs(\n                    download_dir, ireq, wheel_cache\n                )\n\n        return self._dependencies_cache[ireq]\n\n    def _get_project(self, ireq: InstallRequirement) -> _t.Any:\n        \"\"\"\n        Return a dict of a project info from PyPI JSON API for a given\n        InstallRequirement. Return None on HTTP/JSON error or if a package\n        is not found on PyPI server.\n\n        API reference: https://warehouse.readthedocs.io/api-reference/json/\n        \"\"\"\n        package_indexes = (\n            PackageIndex(url=index_url, file_storage_domain=\"\")\n            for index_url in self.finder.search_scope.index_urls\n        )\n        for package_index in package_indexes:\n            url = f\"{package_index.pypi_url}/{ireq.name}/json\"\n            try:\n                response = self.session.get(url)\n            except RequestException as e:\n                log.debug(f\"Fetch package info from PyPI failed: {url}: {e}\")\n                continue\n\n            # Skip this PyPI server, because there is no package\n            # or JSON API might be not supported\n            if response.status_code == 404:\n                continue\n\n            try:\n                data = response.json()\n            except ValueError as e:\n                log.debug(f\"Cannot parse JSON response from PyPI: {url}: {e}\")\n                continue\n            return data\n        return None\n\n    def _get_download_path(self, ireq: InstallRequirement) -> str:\n        \"\"\"\n        Determine the download dir location in a way which avoids name\n        collisions.\n        \"\"\"\n        if ireq.link:\n            salt = hashlib.sha224(ireq.link.url_without_fragment.encode()).hexdigest()\n            # Nest directories to avoid running out of top level dirs on some FS\n            # (see pypi _get_cache_path_parts, which inspired this)\n            return os.path.join(\n                self._download_dir, salt[:2], salt[2:4], salt[4:6], salt[6:]\n            )\n        else:\n            return self._download_dir\n\n    def get_hashes(self, ireq: InstallRequirement) -> set[str]:\n        \"\"\"\n        Given an InstallRequirement, return a set of hashes that represent all\n        of the files for a given requirement. Unhashable requirements return an\n        empty set. Unpinned requirements raise a TypeError.\n        \"\"\"\n\n        if ireq.link:\n            link = ireq.link\n\n            if link.is_vcs or (link.is_file and link.is_existing_dir()):\n                # Return empty set for unhashable requirements.\n                # Unhashable logic modeled on pip's\n                # RequirementPreparer.prepare_linked_requirement\n                return set()\n\n            if is_url_requirement(ireq):\n                # Directly hash URL requirements.\n                # URL requirements may have been previously downloaded and cached\n                # locally by self.resolve_reqs()\n                cached_path = os.path.join(self._get_download_path(ireq), link.filename)\n                if os.path.exists(cached_path):\n                    cached_link = Link(path_to_url(cached_path))\n                else:\n                    cached_link = link\n                return {self._get_file_hash(cached_link)}\n\n        if not is_pinned_requirement(ireq):\n            raise TypeError(f\"Expected pinned requirement, got {ireq}\")\n\n        log.debug(ireq.name)\n\n        with log.indentation():\n            return self._get_req_hashes(ireq)\n\n    def _get_req_hashes(self, ireq: InstallRequirement) -> set[str]:\n        \"\"\"\n        Collects the hashes for all candidates satisfying the given InstallRequirement. Computes\n        the hashes for the candidates that don't have one reported by their index.\n        \"\"\"\n        matching_candidates = self._get_matching_candidates(ireq)\n        pypi_hashes_by_link = self._get_hashes_from_pypi(ireq)\n        pypi_hashes = {\n            pypi_hashes_by_link[candidate.link.url]\n            for candidate in matching_candidates\n            if candidate.link.url in pypi_hashes_by_link\n        }\n        local_hashes = {\n            self._get_file_hash(candidate.link)\n            for candidate in matching_candidates\n            if candidate.link.url not in pypi_hashes_by_link\n        }\n        return pypi_hashes | local_hashes\n\n    def _get_hashes_from_pypi(self, ireq: InstallRequirement) -> dict[str, str]:\n        \"\"\"\n        Builds a mapping from the release URLs to their hashes as reported by the PyPI JSON API\n        for a given InstallRequirement.\n        \"\"\"\n        project = self._get_project(ireq)\n        if project is None:\n            return {}\n\n        _, version, _ = as_tuple(ireq)\n\n        try:\n            release_files = project[\"releases\"][version]\n        except KeyError:\n            log.debug(\"Missing release files on PyPI\")\n            return {}\n\n        try:\n            hashes = {\n                file_[\"url\"]: f\"{FAVORITE_HASH}:{file_['digests'][FAVORITE_HASH]}\"\n                for file_ in release_files\n                if file_[\"packagetype\"] in self.HASHABLE_PACKAGE_TYPES\n            }\n        except KeyError:\n            log.debug(\"Missing digests of release files on PyPI\")\n            return {}\n\n        return hashes\n\n    def _get_matching_candidates(\n        self, ireq: InstallRequirement\n    ) -> set[InstallationCandidate]:\n        \"\"\"\n        Returns all candidates that satisfy the given InstallRequirement.\n        \"\"\"\n        # We need to get all of the candidates that match our current version\n        # pin, these will represent all of the files that could possibly\n        # satisfy this constraint.\n        all_candidates = self.find_all_candidates(ireq.name)\n        candidates_by_version = lookup_table(all_candidates, key=candidate_version)\n        matching_versions = list(\n            ireq.specifier.filter(candidate.version for candidate in all_candidates)\n        )\n        return candidates_by_version[matching_versions[0]]\n\n    def _get_file_hash(self, link: Link) -> str:\n        log.debug(f\"Hashing {link.show_url}\")\n        h = hashlib.new(FAVORITE_HASH)\n        with open_local_or_remote_file(link, self.session) as f:\n            # Chunks to iterate\n            chunks = iter(lambda: f.stream.read(FILE_CHUNK_SIZE), b\"\")\n\n            # Choose a context manager depending on verbosity\n            context_manager: _t.ContextManager[Iterator[bytes]]\n            if log.verbosity >= 1:\n                iter_length = int(f.size / FILE_CHUNK_SIZE) if f.size else None\n                bar_template = f\"{' ' * log.current_indent}  |%(bar)s| %(info)s\"\n                context_manager = progressbar(\n                    chunks,\n                    length=iter_length,\n                    # Make it look like default pip progress bar\n                    fill_char=\"█\",\n                    empty_char=\" \",\n                    bar_template=bar_template,\n                    width=32,\n                )\n            else:\n                context_manager = contextlib.nullcontext(chunks)\n\n            # Iterate over the chosen context manager\n            with context_manager as bar:\n                for chunk in bar:\n                    h.update(chunk)\n        return \":\".join([FAVORITE_HASH, h.hexdigest()])\n\n    @contextmanager\n    def allow_all_wheels(self) -> Iterator[None]:\n        \"\"\"\n        Monkey patches pip.Wheel to allow wheels from all platforms and Python versions.\n\n        This also saves the candidate cache and set a new one, or else the results from\n        the previous non-patched calls will interfere.\n        \"\"\"\n\n        def _wheel_supported(self: Wheel, tags: list[Tag]) -> bool:\n            # Ignore current platform. Support everything.\n            return True\n\n        def _wheel_support_index_min(self: Wheel, tags: list[Tag]) -> int:\n            # All wheels are equal priority for sorting.\n            return 0\n\n        original_wheel_supported = Wheel.supported\n        original_support_index_min = Wheel.support_index_min\n        original_cache = self._available_candidates_cache\n\n        Wheel.supported = _wheel_supported\n        Wheel.support_index_min = _wheel_support_index_min\n        self._available_candidates_cache = {}\n\n        # Finder internally caches results. If we don't clear this cache then it can\n        # contain results from an earlier call when allow_all_wheels wasn't active.\n        # See GH-1532\n        self._clear_finder_cache()\n\n        try:\n            yield\n        finally:\n            Wheel.supported = original_wheel_supported\n            Wheel.support_index_min = original_support_index_min\n            self._available_candidates_cache = original_cache\n\n\n@contextmanager\ndef open_local_or_remote_file(link: Link, session: Session) -> Iterator[FileStream]:\n    \"\"\"\n    Open local or remote file for reading.\n\n    :type link: pip.index.Link\n    :type session: requests.Session\n    :raises ValueError: If link points to a local directory.\n    :return: a context manager to a FileStream with the opened file-like object\n    \"\"\"\n    url = link.url_without_fragment\n\n    if link.is_file:\n        # Local URL\n        local_path = url_to_path(url)\n        if os.path.isdir(local_path):\n            raise ValueError(f\"Cannot open directory for read: {url}\")\n        else:\n            st = os.stat(local_path)\n            with open(local_path, \"rb\") as local_file:\n                yield FileStream(stream=local_file, size=st.st_size)\n    else:\n        # Remote URL\n        headers = {\"Accept-Encoding\": \"identity\"}\n        response = session.get(url, headers=headers, stream=True)\n\n        # Content length must be int or None\n        content_length: int | None\n        try:\n            content_length = int(response.headers[\"content-length\"])\n        except (ValueError, KeyError, TypeError):\n            content_length = None\n\n        try:\n            yield FileStream(stream=response.raw, size=content_length)\n        finally:\n            response.close()\n\n\ndef candidate_version(candidate: InstallationCandidate) -> _BaseVersion:\n    return candidate.version\n"
  },
  {
    "path": "piptools/resolver.py",
    "content": "from __future__ import annotations\n\nimport collections\nimport copy\nimport typing as _t\nfrom abc import ABCMeta, abstractmethod\nfrom collections.abc import Container, Iterable, Iterator\nfrom functools import partial\nfrom itertools import chain, count, groupby\n\nimport click\nfrom pip._internal.exceptions import DistributionNotFound\nfrom pip._internal.operations.build.build_tracker import (\n    get_build_tracker,\n    update_env_context_manager,\n)\nfrom pip._internal.req import InstallRequirement\nfrom pip._internal.resolution.resolvelib.base import Candidate\nfrom pip._internal.resolution.resolvelib.candidates import ExtrasCandidate\nfrom pip._internal.resolution.resolvelib.resolver import Resolver\nfrom pip._internal.utils.logging import indent_log\nfrom pip._internal.utils.temp_dir import TempDirectory, global_tempdir_manager\nfrom pip._vendor.packaging.specifiers import SpecifierSet\nfrom pip._vendor.resolvelib.resolvers import ResolutionImpossible, Result\n\nfrom piptools.cache import DependencyCache\nfrom piptools.repositories.base import BaseRepository\n\nfrom ._compat import canonicalize_name, create_wheel_cache\nfrom ._internal import _pip_api\nfrom .exceptions import PipToolsError\nfrom .logging import log\nfrom .utils import (\n    UNSAFE_PACKAGES,\n    as_tuple,\n    format_requirement,\n    format_specifier,\n    is_pinned_requirement,\n    is_url_requirement,\n    key_from_ireq,\n    key_from_req,\n    omit_list_value,\n    strip_extras,\n)\n\ngreen = partial(click.style, fg=\"green\")\nmagenta = partial(click.style, fg=\"magenta\")\n\n\nclass RequirementSummary:\n    \"\"\"\n    Summary of a requirement's properties for comparison purposes.\n    \"\"\"\n\n    def __init__(self, ireq: InstallRequirement) -> None:\n        self.req = ireq.req\n        self.key = key_from_ireq(ireq)\n        self.extras = frozenset(ireq.extras)\n        self.specifier = ireq.specifier\n\n    def __eq__(self, other: object) -> bool:\n        if not isinstance(other, self.__class__):\n            return NotImplemented\n\n        return (\n            self.key == other.key\n            and self.specifier == other.specifier\n            and self.extras == other.extras\n        )\n\n    def __hash__(self) -> int:\n        return hash((self.key, self.specifier, self.extras))\n\n    def __str__(self) -> str:\n        return repr((self.key, str(self.specifier), sorted(self.extras)))\n\n\ndef combine_install_requirements(\n    ireqs: Iterable[InstallRequirement],\n) -> InstallRequirement:\n    \"\"\"\n    Return a single install requirement that reflects a combination of\n    all the inputs.\n    \"\"\"\n    # We will store the source ireqs in a _source_ireqs attribute;\n    # if any of the inputs have this, then use those sources directly.\n    source_ireqs: list[InstallRequirement] = []\n    for ireq in ireqs:\n        source_ireqs.extend(getattr(ireq, \"_source_ireqs\", [ireq]))\n\n    # Optimization. Don't bother with combination logic.\n    if len(source_ireqs) == 1:\n        return source_ireqs[0]\n\n    link_attrs = {\n        attr: getattr(source_ireqs[0], attr) for attr in (\"link\", \"original_link\")\n    }\n\n    constraint = source_ireqs[0].constraint\n    extras = set(source_ireqs[0].extras)\n    # deepcopy the accumulator req so as to not modify the inputs\n    req = copy.deepcopy(source_ireqs[0].req)\n\n    for ireq in source_ireqs[1:]:\n        # NOTE we may be losing some info on dropped reqs here\n        if req is not None and ireq.req is not None:\n            req.specifier &= ireq.req.specifier\n\n        constraint &= ireq.constraint\n        extras |= ireq.extras\n        if req is not None:\n            req.extras = set(extras)\n\n        for attr_name, attr_val in link_attrs.items():\n            link_attrs[attr_name] = attr_val or getattr(ireq, attr_name)\n\n    # InstallRequirements objects are assumed to come from only one source, and\n    # so they support only a single comes_from entry. This function breaks this\n    # model. As a workaround, we deterministically choose a single source for\n    # the comes_from entry, and add an extra _source_ireqs attribute to keep\n    # track of multiple sources for use within pip-tools.\n    if any(ireq.comes_from is None for ireq in source_ireqs):\n        # None indicates package was directly specified.\n        comes_from = None\n    else:\n        # Populate the comes_from field from one of the sources.\n        # Requirement input order is not stable, so we need to sort:\n        # We choose the shortest entry in order to keep the printed\n        # representation as concise as possible.\n        comes_from = min(\n            (ireq.comes_from for ireq in source_ireqs),\n            key=lambda x: (len(str(x)), str(x)),\n        )\n\n    combined_ireq = _pip_api.copy_install_requirement(\n        template=source_ireqs[0],\n        req=req,\n        comes_from=comes_from,\n        constraint=constraint,\n        extras=extras,\n        **link_attrs,\n    )\n    combined_ireq._source_ireqs = source_ireqs\n\n    return combined_ireq\n\n\nclass BaseResolver(metaclass=ABCMeta):\n    repository: BaseRepository\n    unsafe_constraints: set[InstallRequirement]\n\n    @abstractmethod\n    def resolve(self, max_rounds: int) -> set[InstallRequirement]:\n        r\"\"\"\n        Find concrete package versions for all the given InstallRequirements\n        and their recursive dependencies.\n        :returns: a set of pinned ``InstallRequirement``\\ s.\n        \"\"\"\n\n    def resolve_hashes(\n        self, ireqs: set[InstallRequirement]\n    ) -> dict[InstallRequirement, set[str]]:\n        r\"\"\"\n        Find acceptable hashes for all of the given ``InstallRequirement``\\ s.\n        \"\"\"\n        log.debug(\"\")\n        log.debug(\"Generating hashes:\")\n        with self.repository.allow_all_wheels(), log.indentation():\n            return {ireq: self.repository.get_hashes(ireq) for ireq in ireqs}\n\n    def _filter_out_unsafe_constraints(\n        self,\n        ireqs: set[InstallRequirement],\n        unsafe_packages: Container[str],\n    ) -> None:\n        r\"\"\"\n        Remove from a given set of ``InstallRequirement``\\ s unsafe constraints.\n        \"\"\"\n        for req in ireqs.copy():\n            if req.name in unsafe_packages:\n                self.unsafe_constraints.add(req)\n                ireqs.remove(req)\n\n\nclass LegacyResolver(BaseResolver):\n    \"\"\"\n    Wrapper for the (deprecated) legacy dependency resolver.\n    \"\"\"\n\n    def __init__(\n        self,\n        constraints: Iterable[InstallRequirement],\n        existing_constraints: dict[str, InstallRequirement],\n        repository: BaseRepository,\n        cache: DependencyCache,\n        prereleases: bool | None = False,\n        clear_caches: bool = False,\n        allow_unsafe: bool = False,\n        unsafe_packages: set[str] | None = None,\n    ) -> None:\n        \"\"\"Initialize LegacyResolver.\n\n        :param constraints: the constraints given\n        :type constraints: Iterable[InstallRequirement]\n        :param existing_constraints: constraints already present\n        :param repository: the repository to get the constraints from\n        :type repository: BaseRepository\n        :param cache: the cache to be used\n        :param prereleases: whether prereleases should be taken into account when resolving\n            (default is :py:data:`False`)\n        :param clear_caches: whether to clear repository and dependency caches before resolving\n            (default is :py:data:`False`)\n        :param allow_unsafe: whether unsafe packages should be allowed in the resulting requirements\n            (default is :py:data:`False`)\n        :param unsafe_packages: packages to be considered as unsafe\n            (default is :py:data:`None`)\n        :type unsafe_packages: set[str]\n        :raises: ``PipToolsError`` if the legacy resolver is not enabled\n        \"\"\"\n        self.our_constraints = set(constraints)\n        self.their_constraints: set[InstallRequirement] = set()\n        self.repository = repository\n        self.dependency_cache = cache\n        self.prereleases = prereleases\n        self.clear_caches = clear_caches\n        self.allow_unsafe = allow_unsafe\n        self.unsafe_constraints: set[InstallRequirement] = set()\n        self.unsafe_packages = unsafe_packages or UNSAFE_PACKAGES\n\n        options = self.repository.options\n        if \"legacy-resolver\" not in options.deprecated_features_enabled:\n            raise PipToolsError(\"Legacy resolver deprecated feature must be enabled.\")\n\n        # Make sure there is no enabled 2020-resolver\n        options.features_enabled = omit_list_value(\n            options.features_enabled, \"2020-resolver\"\n        )\n\n    @property\n    def constraints(self) -> set[InstallRequirement]:\n        return set(\n            self._group_constraints(chain(self.our_constraints, self.their_constraints))\n        )\n\n    def resolve(self, max_rounds: int = 10) -> set[InstallRequirement]:\n        r\"\"\"\n        Find concrete package versions for all the given ``InstallRequirement``\\ s\n        and their recursive dependencies and return a set of pinned\n        ``InstallRequirement``\\ s.\n\n        Resolves constraints one round at a time, until they don't change\n        anymore.\n\n        :param max_rounds: break out of resolution process after the given number of rounds\n            to prevent infinite loops (default is 10)\n        \"\"\"\n        if self.clear_caches:\n            self.dependency_cache.clear()\n            self.repository.clear_caches()\n\n        # Ignore existing packages\n        with update_env_context_manager(PIP_EXISTS_ACTION=\"i\"):\n            for current_round in count(start=1):  # pragma: no branch\n                if current_round > max_rounds:\n                    raise RuntimeError(\n                        \"No stable configuration of concrete packages \"\n                        \"could be found for the given constraints after \"\n                        \"{max_rounds} rounds of resolving.\\n\"\n                        \"This is likely a bug.\".format(max_rounds=max_rounds)\n                    )\n\n                log.debug(\"\")\n                log.debug(magenta(f\"{f'ROUND {current_round}':^60}\"))\n                has_changed, best_matches = self._resolve_one_round()\n                log.debug(\"-\" * 60)\n                log.debug(\n                    \"Result of round {}: {}\".format(\n                        current_round,\n                        \"not stable\" if has_changed else \"stable, done\",\n                    )\n                )\n                if not has_changed:\n                    break\n\n        # Only include hard requirements and not pip constraints\n        results = {req for req in best_matches if not req.constraint}\n\n        # Filter out unsafe requirements.\n        if not self.allow_unsafe:\n            self._filter_out_unsafe_constraints(\n                ireqs=results,\n                unsafe_packages=self.unsafe_packages,\n            )\n\n        return results\n\n    def _group_constraints(\n        self, constraints: Iterable[InstallRequirement]\n    ) -> Iterator[InstallRequirement]:\n        \"\"\"\n        Group constraints (remember, InstallRequirements!) by their key name.\n\n        Then combine their SpecifierSets into a single InstallRequirement per\n        package.  For example, given the following constraints:\n\n            Django<1.9,>=1.4.2\n            django~=1.5\n            Flask~=0.7\n\n        This will be combined into a single entry per package:\n\n            django~=1.5,<1.9,>=1.4.2\n            flask~=0.7\n\n        \"\"\"\n        constraints = list(constraints)\n\n        for ireq in constraints:\n            if ireq.name is None:\n                # get_dependencies has side-effect of assigning name to ireq\n                # (so we can group by the name below).\n                self.repository.get_dependencies(ireq)\n\n        # Sort first by name, i.e. the groupby key. Then within each group,\n        # sort editables first.\n        # This way, we don't bother with combining editables, since the first\n        # ireq will be editable, if one exists.\n        for _, ireqs in groupby(\n            sorted(constraints, key=(lambda x: (key_from_ireq(x), not x.editable))),\n            key=key_from_ireq,\n        ):\n            yield combine_install_requirements(ireqs)\n\n    def _resolve_one_round(self) -> tuple[bool, set[InstallRequirement]]:\n        \"\"\"\n        Resolve one level of the current constraints.\n\n        This is achieved by finding the best match for each package\n        in the repository and adding all requirements for those best\n        package versions.  Some of these constraints may be new\n        or updated.\n\n        :returns: whether new constraints appeared in this round.  If no\n            constraints were added or changed, this indicates a stable\n            configuration.\n        \"\"\"\n        # Sort this list for readability of terminal output\n        constraints = sorted(self.constraints, key=key_from_ireq)\n\n        log.debug(\"Current constraints:\")\n        with log.indentation():\n            for constraint in constraints:\n                log.debug(str(constraint))\n\n        log.debug(\"\")\n        log.debug(\"Finding the best candidates:\")\n        with log.indentation():\n            best_matches = {self.get_best_match(ireq) for ireq in constraints}\n\n        # Find the new set of secondary dependencies\n        log.debug(\"\")\n        log.debug(\"Finding secondary dependencies:\")\n\n        their_constraints: list[InstallRequirement] = []\n        with log.indentation():\n            for best_match in best_matches:\n                their_constraints.extend(self._iter_dependencies(best_match))\n        # Grouping constraints to make clean diff between rounds\n        theirs = set(self._group_constraints(their_constraints))\n\n        # NOTE: We need to compare RequirementSummary objects, since\n        # InstallRequirement does not define equality\n        diff = {RequirementSummary(t) for t in theirs} - {\n            RequirementSummary(t) for t in self.their_constraints\n        }\n        removed = {RequirementSummary(t) for t in self.their_constraints} - {\n            RequirementSummary(t) for t in theirs\n        }\n\n        has_changed = len(diff) > 0 or len(removed) > 0\n        if has_changed:\n            log.debug(\"\")\n            log.debug(\"New dependencies found in this round:\")\n            with log.indentation():\n                for new_dependency in sorted(diff, key=key_from_ireq):\n                    log.debug(f\"adding {new_dependency}\")\n            log.debug(\"Removed dependencies in this round:\")\n            with log.indentation():\n                for removed_dependency in sorted(removed, key=key_from_ireq):\n                    log.debug(f\"removing {removed_dependency}\")\n\n        # Store the last round's results in the their_constraints\n        self.their_constraints = theirs\n        return has_changed, best_matches\n\n    def get_best_match(self, ireq: InstallRequirement) -> InstallRequirement:\n        \"\"\"\n        Return a (pinned or editable) InstallRequirement.\n\n        This indicates the best match to use for the given\n        InstallRequirement (in the form of an InstallRequirement).\n\n        Example:\n        Given the constraint Flask>=0.10, may return Flask==0.10.1 at\n        a certain moment in time.\n\n        Pinned requirements will always return themselves, i.e.\n\n            Flask==0.10.1 => Flask==0.10.1\n\n        \"\"\"\n        if ireq.editable or is_url_requirement(ireq):\n            # NOTE: it's much quicker to immediately return instead of\n            # hitting the index server\n            best_match = ireq\n        elif is_pinned_requirement(ireq):\n            # NOTE: it's much quicker to immediately return instead of\n            # hitting the index server\n            best_match = ireq\n        elif ireq.constraint:\n            # NOTE: This is not a requirement (yet) and does not need\n            # to be resolved\n            best_match = ireq\n        else:\n            best_match = self.repository.find_best_match(\n                ireq, prereleases=self.prereleases\n            )\n\n        # Format the best match\n        log.debug(\n            \"found candidate {} (constraint was {})\".format(\n                format_requirement(best_match), format_specifier(ireq)\n            )\n        )\n        best_match.comes_from = ireq.comes_from\n        if hasattr(ireq, \"_source_ireqs\"):\n            best_match._source_ireqs = ireq._source_ireqs\n        return best_match\n\n    def _iter_dependencies(\n        self, ireq: InstallRequirement\n    ) -> Iterator[InstallRequirement]:\n        \"\"\"\n        Emit all secondary dependencies for an ireq.\n\n        Given a pinned, url, or editable InstallRequirement, collects all the\n        secondary dependencies for them, either by looking them up in a local\n        cache, or by reaching out to the repository.\n\n        Editable requirements will never be looked up, as they may have\n        changed at any time.\n        \"\"\"\n        # Pip does not resolve dependencies of constraints. We skip handling\n        # constraints here as well to prevent the cache from being polluted.\n        # Constraints that are later determined to be dependencies will be\n        # marked as non-constraints in later rounds by\n        # `combine_install_requirements`, and will be properly resolved.\n        # See https://github.com/pypa/pip/\n        # blob/6896dfcd831330c13e076a74624d95fa55ff53f4/src/pip/_internal/\n        # legacy_resolve.py#L325\n        if ireq.constraint:\n            return\n\n        if ireq.editable or is_url_requirement(ireq):\n            dependencies = self.repository.get_dependencies(ireq)\n            # Don't just yield from above. Instead, use the same `markers`-stripping\n            # behavior as we have for cached dependencies below.\n            dependency_strings = sorted(str(ireq.req) for ireq in dependencies)\n            yield from self._ireqs_of_dependencies(ireq, dependency_strings)\n            return\n        elif not is_pinned_requirement(ireq):\n            raise TypeError(f\"Expected pinned or editable requirement, got {ireq}\")\n\n        # Now, either get the dependencies from the dependency cache (for\n        # speed), or reach out to the external repository to\n        # download and inspect the package version and get dependencies\n        # from there\n        if ireq not in self.dependency_cache:\n            log.debug(\n                f\"{format_requirement(ireq)} not in cache, need to check index\",\n                fg=\"yellow\",\n            )\n            dependencies = self.repository.get_dependencies(ireq)\n            self.dependency_cache[ireq] = sorted(str(ireq.req) for ireq in dependencies)\n\n        # Example: ['Werkzeug>=0.9', 'Jinja2>=2.4']\n        dependency_strings = self.dependency_cache[ireq]\n        yield from self._ireqs_of_dependencies(ireq, dependency_strings)\n\n    def _ireqs_of_dependencies(\n        self, ireq: InstallRequirement, dependency_strings: list[str]\n    ) -> Iterator[InstallRequirement]:\n        log.debug(\n            \"{:25} requires {}\".format(\n                format_requirement(ireq),\n                \", \".join(sorted(dependency_strings, key=lambda s: s.lower())) or \"-\",\n            )\n        )\n        # This yields new InstallRequirements that are similar to those that\n        # produced the dependency_strings, but they lack `markers` on their\n        # underlying Requirements:\n        for dependency_string in dependency_strings:\n            yield _pip_api.create_install_requirement_from_line(\n                dependency_string, constraint=ireq.constraint, comes_from=ireq\n            )\n\n\nclass BacktrackingResolver(BaseResolver):\n    \"\"\"A wrapper for the backtracking (or 2020) resolver.\"\"\"\n\n    def __init__(\n        self,\n        constraints: Iterable[InstallRequirement],\n        existing_constraints: dict[str, InstallRequirement],\n        repository: BaseRepository,\n        allow_unsafe: bool = False,\n        unsafe_packages: set[str] | None = None,\n        **kwargs: _t.Any,\n    ) -> None:\n        self.constraints = list(constraints)\n        self.repository = repository\n        self.allow_unsafe = allow_unsafe\n        self.unsafe_packages = unsafe_packages or UNSAFE_PACKAGES\n\n        options = self.options = self.repository.options\n        self.session = self.repository.session\n        self.finder = self.repository.finder\n        self.command = self.repository.command\n        self.unsafe_constraints: set[InstallRequirement] = set()\n\n        self.existing_constraints = existing_constraints\n\n        # Categorize InstallRequirements into sets by key\n        constraints_sets: collections.defaultdict[str, set[InstallRequirement]] = (\n            collections.defaultdict(set)\n        )\n        for ireq in constraints:\n            constraints_sets[key_from_ireq(ireq)].add(ireq)\n        # Collapse each set of InstallRequirements using combine_install_requirements\n        self._constraints_map = {\n            ireq_key: combine_install_requirements(ireqs)\n            for ireq_key, ireqs in constraints_sets.items()\n        }\n\n        # Make sure there is no enabled legacy resolver\n        options.deprecated_features_enabled = omit_list_value(\n            options.deprecated_features_enabled, \"legacy-resolver\"\n        )\n\n    def resolve(self, max_rounds: int = 10) -> set[InstallRequirement]:\n        r\"\"\"\n        Resolve given ireqs.\n\n        Find concrete package versions for all the given InstallRequirements\n        and their recursive dependencies.\n\n        :returns: A set of pinned ``InstallRequirement``\\ s.\n        \"\"\"\n        with (\n            update_env_context_manager(PIP_EXISTS_ACTION=\"i\"),\n            get_build_tracker() as build_tracker,\n            global_tempdir_manager(),\n            indent_log(),\n        ):\n            # Mark direct/primary/user_supplied packages\n            for ireq in self.constraints:\n                if ireq.constraint:\n                    ireq.extras = set()  # pip does not support extras in constraints\n                ireq.user_supplied = True\n\n            # Pass compiled requirements from `requirements.txt`\n            # as constraints to resolver\n            compatible_existing_constraints: dict[str, InstallRequirement] = {}\n            for ireq in self.existing_constraints.values():\n                # Skip if the compiled install requirement conflicts with\n                # the primary install requirement.\n                primary_ireq = self._constraints_map.get(key_from_ireq(ireq))\n                if primary_ireq is not None:\n                    _, version, _ = as_tuple(ireq)\n                    prereleases = ireq.specifier.prereleases\n                    if not primary_ireq.specifier.contains(version, prereleases):\n                        continue\n\n                ireq.extras = set()\n                ireq.constraint = True\n                ireq.user_supplied = False\n                compatible_existing_constraints[key_from_ireq(ireq)] = ireq\n\n            wheel_cache = create_wheel_cache(\n                cache_dir=self.options.cache_dir,\n                format_control=self.options.format_control,\n            )\n\n            temp_dir = TempDirectory(\n                delete=not self.options.no_clean,\n                kind=\"resolve\",\n                globally_managed=True,\n            )\n\n            preparer_kwargs = {\n                \"temp_build_dir\": temp_dir,\n                \"options\": self.options,\n                \"session\": self.session,\n                \"finder\": self.finder,\n                \"use_user_site\": False,\n                \"build_tracker\": build_tracker,\n            }\n            preparer = self.command.make_requirement_preparer(**preparer_kwargs)\n\n            extra_resolver_kwargs = {}\n            if _pip_api.PIP_VERSION_MAJOR_MINOR < (25, 3):  # pragma: <3.9 cover\n                # Ref: https://github.com/jazzband/pip-tools/issues/2252\n                extra_resolver_kwargs[\"use_pep517\"] = self.options.use_pep517\n\n            resolver = self.command.make_resolver(\n                preparer=preparer,\n                finder=self.finder,\n                options=self.options,\n                wheel_cache=wheel_cache,\n                use_user_site=False,\n                ignore_installed=True,\n                ignore_requires_python=False,\n                force_reinstall=False,\n                upgrade_strategy=\"to-satisfy-only\",\n                **extra_resolver_kwargs,\n            )\n\n            self.command.trace_basic_info(self.finder)\n\n            for current_round in count(start=1):  # pragma: no branch\n                if current_round > max_rounds:\n                    raise RuntimeError(\n                        \"No stable configuration of concrete packages \"\n                        \"could be found for the given constraints after \"\n                        f\"{max_rounds} rounds of resolving.\\n\"\n                        \"This is likely a bug.\"\n                    )\n\n                log.debug(\"\")\n                log.debug(magenta(f\"{f'ROUND {current_round}':^60}\"))\n\n                is_resolved = self._do_resolve(\n                    resolver=resolver,\n                    compatible_existing_constraints=compatible_existing_constraints,\n                )\n                if is_resolved:\n                    break\n\n        resolver_result = resolver._result\n        assert isinstance(resolver_result, Result)\n\n        # Prepare set of install requirements from resolver result.\n        result_ireqs = self._get_install_requirements(resolver_result=resolver_result)\n\n        # Filter out unsafe requirements.\n        if not self.allow_unsafe:\n            self._filter_out_unsafe_constraints(\n                ireqs=result_ireqs,\n                unsafe_packages=self.unsafe_packages,\n            )\n\n        return result_ireqs\n\n    def _do_resolve(\n        self,\n        resolver: Resolver,\n        compatible_existing_constraints: dict[str, InstallRequirement],\n    ) -> bool:\n        \"\"\"\n        Resolve dependencies based on resolvelib ``Resolver``.\n\n        :returns: :py:data:`True` on successful resolution, otherwise removes\n            problematic requirements from existing constraints and\n            returns :py:data:`False`.\n        \"\"\"\n        try:\n            resolver.resolve(\n                root_reqs=self.constraints\n                + list(compatible_existing_constraints.values()),\n                check_supported_wheels=not self.options.target_dir,\n            )\n        except DistributionNotFound as e:\n            cause_exc = e.__cause__\n            if cause_exc is None:\n                raise\n\n            if not isinstance(cause_exc, ResolutionImpossible):\n                raise\n\n            # Collect all incompatible install requirement names\n            cause_ireq_names = {\n                strip_extras(key_from_req(cause.requirement))\n                for cause in cause_exc.causes\n            }\n\n            # Looks like resolution is impossible, try to fix\n            for cause_ireq_name in cause_ireq_names:\n                # Find the cause requirement in existing requirements,\n                # otherwise raise error\n                cause_existing_ireq = compatible_existing_constraints.get(\n                    cause_ireq_name\n                )\n                if cause_existing_ireq is None:\n                    raise\n\n                # Remove existing incompatible constraint that causes error\n                log.warning(\n                    f\"Discarding {cause_existing_ireq} to proceed the resolution\"\n                )\n                del compatible_existing_constraints[cause_ireq_name]\n\n            return False\n\n        return True\n\n    def _get_install_requirements(\n        self, resolver_result: Result\n    ) -> set[InstallRequirement]:\n        \"\"\"Return a set of install requirements from resolver results.\"\"\"\n        result_ireqs: dict[str, InstallRequirement] = {}\n\n        # Get reverse requirements from the resolver result graph.\n        reverse_dependencies = self._get_reverse_dependencies(resolver_result)\n\n        # Transform candidates to install requirements\n        resolved_candidates = tuple(resolver_result.mapping.values())\n        for candidate in resolved_candidates:\n            ireq = self._get_install_requirement_from_candidate(\n                candidate=candidate,\n                reverse_dependencies=reverse_dependencies,\n            )\n            if ireq is None:\n                continue\n\n            project_name = canonicalize_name(candidate.project_name)\n            result_ireqs[project_name] = ireq\n\n        # Merge extras to install requirements\n        extras_candidates = (\n            candidate\n            for candidate in resolved_candidates\n            if isinstance(candidate, ExtrasCandidate)\n        )\n        for extras_candidate in extras_candidates:\n            project_name = canonicalize_name(extras_candidate.project_name)\n            ireq = result_ireqs[project_name]\n            ireq.extras |= extras_candidate.extras\n            ireq.req.extras |= extras_candidate.extras\n\n        return set(result_ireqs.values())\n\n    @staticmethod\n    def _get_reverse_dependencies(\n        resolver_result: Result,\n    ) -> dict[str, set[str]]:\n        reverse_dependencies: collections.defaultdict[str, set[str]] = (\n            collections.defaultdict(set)\n        )\n\n        for candidate in resolver_result.mapping.values():\n            stripped_name = strip_extras(canonicalize_name(candidate.name))\n\n            for parent_name in resolver_result.graph.iter_parents(candidate.name):\n                # Skip root dependency which is always None\n                if parent_name is None:\n                    continue\n\n                # Skip a dependency that equals to the candidate. This could be\n                # the dependency with extras.\n                stripped_parent_name = strip_extras(canonicalize_name(parent_name))\n                if stripped_name == stripped_parent_name:\n                    continue\n\n                reverse_dependencies[stripped_name].add(stripped_parent_name)\n\n        return dict(reverse_dependencies)\n\n    def _get_install_requirement_from_candidate(\n        self, candidate: Candidate, reverse_dependencies: dict[str, set[str]]\n    ) -> InstallRequirement | None:\n        ireq = candidate.get_install_requirement()\n        if ireq is None:\n            return None\n\n        # Determine a pin operator\n        version_pin_operator = \"==\"\n        version_as_str = str(candidate.version)\n        for specifier in ireq.specifier:\n            if specifier.operator == \"===\" and specifier.version == version_as_str:\n                version_pin_operator = \"===\"\n                break\n\n        # Prepare pinned install requirement. Copy it from candidate's install\n        # requirement so that it could be mutated later.\n        pinned_ireq = _pip_api.copy_install_requirement(\n            template=ireq,\n            # The link this candidate \"originates\" from. This is different\n            # from ``ireq.link`` when the link is found in the wheel cache.\n            # ``ireq.link`` would point to the wheel cache, while this points\n            # to the found remote link (e.g. from pypi.org).\n            link=candidate.source_link,\n        )\n\n        # Canonicalize name\n        assert ireq.name is not None\n        pinned_ireq.req.name = canonicalize_name(ireq.name)\n\n        # Pin requirement to a resolved version\n        pinned_ireq.req.specifier = SpecifierSet(\n            f\"{version_pin_operator}{candidate.version}\"\n        )\n\n        # Save reverse dependencies for annotation\n        ireq_key = key_from_ireq(ireq)\n        pinned_ireq._required_by = reverse_dependencies.get(ireq_key, set())\n\n        # Save sources for annotation\n        constraint_ireq = self._constraints_map.get(ireq_key)\n        if constraint_ireq is not None:\n            if hasattr(constraint_ireq, \"_source_ireqs\"):\n                # If the constraint is combined (has _source_ireqs), use those\n                pinned_ireq._source_ireqs = constraint_ireq._source_ireqs\n            else:\n                # Otherwise (the constraint is not combined) it is the source\n                pinned_ireq._source_ireqs = [constraint_ireq]\n\n        return pinned_ireq\n"
  },
  {
    "path": "piptools/scripts/__init__.py",
    "content": ""
  },
  {
    "path": "piptools/scripts/_deprecations.py",
    "content": "\"\"\"Module to deprecate script arguments.\"\"\"\n\nfrom __future__ import annotations\n\nfrom .._internal import _pip_api\nfrom ..logging import log\n\n\ndef filter_deprecated_pip_args(args: list[str]) -> list[str]:\n    \"\"\"\n    Warn about and drop pip args that are no longer supported by pip.\n\n    Currently drops:\n\n    - ``--use-pep517``\n    - ``--no-use-pep517``\n    - ``--global-option``\n    - ``--build-option``\n    \"\"\"\n    if _pip_api.PIP_VERSION_MAJOR_MINOR < (25, 3):  # pragma: <3.9 cover\n        return args\n\n    deprecation_mapping = {\n        \"--use-pep517\": \"Pip always uses PEP 517 for building projects now.\",\n        \"--no-use-pep517\": \"Pip always uses PEP 517 for building projects now.\",\n        \"--global-option\": (\n            \"--config-setting is now the only way to pass options to the build backend.\"\n        ),\n        \"--build-option\": (\n            \"--config-setting is now the only way to pass options to the build backend.\"\n        ),\n    }\n    supported_args = []\n    for arg in args:\n        opt_key = arg.split(\"=\")[0]\n        try:\n            warn_msg = deprecation_mapping[opt_key]\n        except KeyError:\n            supported_args.append(arg)\n        else:\n            log.warning(\n                \"WARNING: \"\n                f\"{arg} is no longer supported by pip and is deprecated in pip-tools. \"\n                \"This option is ignored and will result in errors in a future release. \"\n                f\"{warn_msg}\"\n            )\n\n    return supported_args\n"
  },
  {
    "path": "piptools/scripts/compile.py",
    "content": "from __future__ import annotations\n\nimport itertools\nimport os\nimport shlex\nimport sys\nimport typing as _t\nfrom pathlib import Path\n\nimport click\nfrom build import BuildBackendException\nfrom click.utils import LazyFile, safecall\nfrom pip._internal.req import InstallRequirement\nfrom pip._internal.utils.misc import redact_auth_from_url\n\nfrom .._compat import canonicalize_name, parse_requirements, tempfile_compat\nfrom .._internal import _pip_api\nfrom ..build import ProjectMetadata, build_project_metadata\nfrom ..cache import DependencyCache\nfrom ..exceptions import NoCandidateFound, PipToolsError\nfrom ..logging import log\nfrom ..repositories import LocalRequirementsRepository, PyPIRepository\nfrom ..repositories.base import BaseRepository\nfrom ..resolver import BacktrackingResolver, LegacyResolver\nfrom ..utils import (\n    dedup,\n    drop_extras,\n    is_pinned_requirement,\n    key_from_ireq,\n)\nfrom ..writer import OutputWriter\nfrom . import options\nfrom ._deprecations import filter_deprecated_pip_args\nfrom .options import BuildTargetT\n\nDEFAULT_REQUIREMENTS_FILES = (\n    \"requirements.in\",\n    \"setup.py\",\n    \"pyproject.toml\",\n    \"setup.cfg\",\n)\nDEFAULT_REQUIREMENTS_FILE = \"requirements.in\"\nDEFAULT_REQUIREMENTS_OUTPUT_FILE = \"requirements.txt\"\nMETADATA_FILENAMES = frozenset({\"setup.py\", \"setup.cfg\", \"pyproject.toml\"})\n\n\ndef _determine_linesep(\n    strategy: str = \"preserve\", filenames: tuple[str, ...] = ()\n) -> str:\n    \"\"\"\n    Determine and return linesep string for OutputWriter to use.\n\n    Valid strategies: \"LF\", \"CRLF\", \"native\", \"preserve\"\n    When preserving, files are checked in order for existing newlines.\n    \"\"\"\n    if strategy == \"preserve\":\n        for fname in filenames:\n            try:\n                with open(fname, \"rb\") as existing_file:\n                    existing_text = existing_file.read()\n            except FileNotFoundError:\n                continue\n            if b\"\\r\\n\" in existing_text:\n                strategy = \"CRLF\"\n                break\n            elif b\"\\n\" in existing_text:\n                strategy = \"LF\"\n                break\n    return {\n        \"native\": os.linesep,\n        \"LF\": \"\\n\",\n        \"CRLF\": \"\\r\\n\",\n        \"preserve\": \"\\n\",\n    }[strategy]\n\n\nCOMPILE_EPILOG = \"\"\"\\b\nExamples:\n\\b\n    Compile requirements.in to requirements.txt:\n    $ pip-compile\n\\b\n    Upgrade all packages to their latest versions:\n    $ pip-compile --upgrade\n\\b\n    Upgrade specific packages:\n    $ pip-compile -P django -P requests\n\\b\n    Include package hashes for extra security:\n    $ pip-compile --generate-hashes\n\\b\n    Compile with optional extras:\n    $ pip-compile --extra dev pyproject.toml\n\"\"\"\n\n\n@click.command(name=\"pip-compile\")\n@click.pass_context\n@options.help_option(epilog=COMPILE_EPILOG)\n@options.version\n@options.color\n@options.verbose\n@options.quiet\n@options.dry_run\n@options.pre\n@options.rebuild\n@options.extra\n@options.all_extras\n@options.find_links\n@options.index_url\n@options.no_index\n@options.extra_index_url\n@options.cert\n@options.client_cert\n@options.trusted_host\n@options.header\n@options.emit_trusted_host\n@options.annotate\n@options.annotation_style\n@options.upgrade\n@options.upgrade_package\n@options.output_file\n@options.newline\n@options.allow_unsafe\n@options.strip_extras\n@options.generate_hashes\n@options.reuse_hashes\n@options.max_rounds\n@options.src_files\n@options.build_isolation\n@options.emit_find_links\n@options.cache_dir\n@options.pip_args\n@options.resolver\n@options.emit_index_url\n@options.emit_options\n@options.unsafe_package\n@options.config\n@options.no_config\n@options.constraint\n@options.build_deps_for\n@options.all_build_deps\n@options.only_build_deps\ndef cli(\n    ctx: click.Context,\n    color: bool | None,\n    verbose: int,\n    quiet: int,\n    dry_run: bool,\n    pre: bool,\n    rebuild: bool,\n    extras: tuple[str, ...],\n    all_extras: bool,\n    find_links: tuple[str, ...],\n    index_url: str,\n    no_index: bool,\n    extra_index_url: tuple[str, ...],\n    cert: str | None,\n    client_cert: str | None,\n    trusted_host: tuple[str, ...],\n    header: bool,\n    emit_trusted_host: bool,\n    annotate: bool,\n    annotation_style: str,\n    upgrade: bool,\n    upgrade_packages: tuple[str, ...],\n    output_file: LazyFile | _t.IO[_t.Any] | None,\n    newline: str,\n    allow_unsafe: bool,\n    strip_extras: bool | None,\n    generate_hashes: bool,\n    reuse_hashes: bool,\n    src_files: tuple[str, ...],\n    max_rounds: int,\n    build_isolation: bool,\n    emit_find_links: bool,\n    cache_dir: str,\n    pip_args_str: str | None,\n    resolver_name: str,\n    emit_index_url: bool,\n    emit_options: bool,\n    unsafe_package: tuple[str, ...],\n    config: Path | None,\n    no_config: bool,\n    constraint: tuple[str, ...],\n    build_deps_targets: tuple[BuildTargetT, ...],\n    all_build_deps: bool,\n    only_build_deps: bool,\n) -> None:\n    \"\"\"\n    Compile requirements.txt from source files.\n\n    Valid sources are requirements.in, pyproject.toml, setup.cfg,\n    or setup.py specs.\n    \"\"\"\n    if color is not None:\n        ctx.color = color\n    log.verbosity = verbose - quiet\n\n    # If ``src-files` was not provided as an input, but rather as config,\n    # it will be part of the click context ``ctx``.\n    # However, if ``src_files`` is specified, then we want to use that.\n    if not src_files and ctx.default_map and \"src_files\" in ctx.default_map:\n        src_files = ctx.default_map[\"src_files\"]\n\n    if all_build_deps and build_deps_targets:\n        raise click.BadParameter(\n            \"--build-deps-for has no effect when used with --all-build-deps\"\n        )\n    elif all_build_deps:\n        build_deps_targets = options.ALL_BUILD_TARGETS\n\n    if only_build_deps and not build_deps_targets:\n        raise click.BadParameter(\n            \"--only-build-deps requires either --build-deps-for or --all-build-deps\"\n        )\n    if only_build_deps and (extras or all_extras):\n        raise click.BadParameter(\n            \"--only-build-deps cannot be used with any of --extra, --all-extras\"\n        )\n\n    if len(src_files) == 0:\n        for file_path in DEFAULT_REQUIREMENTS_FILES:\n            if os.path.exists(file_path):\n                src_files = (file_path,)\n                break\n        else:\n            raise click.BadParameter(\n                (\n                    \"If you do not specify an input file, the default is one of: {}\"\n                ).format(\", \".join(DEFAULT_REQUIREMENTS_FILES))\n            )\n\n    if all_extras and extras:\n        msg = \"--extra has no effect when used with --all-extras\"\n        raise click.BadParameter(msg)\n\n    if not output_file:\n        # An output file must be provided for stdin\n        if src_files == (\"-\",):\n            raise click.BadParameter(\"--output-file is required if input is from stdin\")\n        # Use default requirements output file if there is a setup.py the source file\n        elif os.path.basename(src_files[0]) in METADATA_FILENAMES:\n            file_name = os.path.join(\n                os.path.dirname(src_files[0]), DEFAULT_REQUIREMENTS_OUTPUT_FILE\n            )\n        # An output file must be provided if there are multiple source files\n        elif len(src_files) > 1:\n            raise click.BadParameter(\n                \"--output-file is required if two or more input files are given.\"\n            )\n        # Otherwise derive the output file from the source file\n        else:\n            base_name = src_files[0].rsplit(\".\", 1)[0]\n            file_name = base_name + \".txt\"\n\n        output_file = click.open_file(file_name, \"w+b\", atomic=True, lazy=True)\n\n        # Close the file at the end of the context execution\n        assert output_file is not None\n        # only LazyFile has close_intelligently, newer _t.IO[_t.Any] does not\n        if isinstance(output_file, LazyFile):  # pragma: no cover\n            ctx.call_on_close(safecall(output_file.close_intelligently))\n\n    if output_file.name != \"-\" and output_file.name in src_files:\n        raise click.BadArgumentUsage(\n            f\"input and output filenames must not be matched: {output_file.name}\"\n        )\n\n    if config:\n        log.debug(f\"Using pip-tools configuration defaults found in '{config !s}'.\")\n\n    if resolver_name == \"legacy\":\n        log.warning(\n            \"WARNING: the legacy dependency resolver is deprecated and will be removed\"\n            \" in future versions of pip-tools.\"\n        )\n\n    ###\n    # Setup\n    ###\n\n    right_args = shlex.split(pip_args_str or \"\")\n    pip_args = []\n    for link in find_links:\n        pip_args.extend([\"-f\", link])\n    if index_url:\n        pip_args.extend([\"-i\", index_url])\n    if no_index:\n        pip_args.extend([\"--no-index\"])\n    for extra_index in extra_index_url:\n        pip_args.extend([\"--extra-index-url\", extra_index])\n    if cert:\n        pip_args.extend([\"--cert\", cert])\n    if client_cert:\n        pip_args.extend([\"--client-cert\", client_cert])\n    if pre:\n        pip_args.extend([\"--pre\"])\n    for host in trusted_host:\n        pip_args.extend([\"--trusted-host\", host])\n    if not build_isolation:\n        pip_args.append(\"--no-build-isolation\")\n    if resolver_name == \"legacy\":\n        pip_args.extend([\"--use-deprecated\", \"legacy-resolver\"])\n    if resolver_name == \"backtracking\" and cache_dir:\n        pip_args.extend([\"--cache-dir\", cache_dir])\n    pip_args.extend(right_args)\n    pip_args = filter_deprecated_pip_args(pip_args)\n\n    repository: BaseRepository\n    repository = PyPIRepository(pip_args, cache_dir=cache_dir)\n\n    # Parse all constraints coming from --upgrade-package/-P\n    upgrade_reqs_gen = (\n        _pip_api.create_install_requirement_from_line(pkg) for pkg in upgrade_packages\n    )\n    upgrade_install_reqs = {\n        key_from_ireq(install_req): install_req for install_req in upgrade_reqs_gen\n    }\n\n    # Exclude packages from --upgrade-package/-P from the existing constraints\n    existing_pins = {}\n\n    # Proxy with a LocalRequirementsRepository if --upgrade is not specified\n    # (= default invocation)\n    output_file_exists = os.path.exists(output_file.name)\n    if not upgrade and output_file_exists:\n        output_file_is_empty = os.path.getsize(output_file.name) == 0\n        if upgrade_install_reqs and output_file_is_empty:\n            log.warning(\n                f\"WARNING: the output file {output_file.name} exists but is empty. \"\n                \"Pip-tools cannot upgrade only specific packages (using -P/--upgrade-package) \"\n                \"without an existing pin file to provide constraints. \"\n                \"This often occurs if you redirect standard output to your output file, \"\n                \"as any existing content is truncated.\"\n            )\n\n        # Use a temporary repository to ensure outdated(removed) options from\n        # existing requirements.txt wouldn't get into the current repository.\n        tmp_repository = PyPIRepository(pip_args, cache_dir=cache_dir)\n        ireqs = parse_requirements(\n            output_file.name,\n            finder=tmp_repository.finder,\n            session=tmp_repository.session,\n            options=tmp_repository.options,\n        )\n\n        for ireq in filter(is_pinned_requirement, ireqs):\n            key = key_from_ireq(ireq)\n            if key not in upgrade_install_reqs:\n                existing_pins[key] = ireq\n        repository = LocalRequirementsRepository(\n            existing_pins, repository, reuse_hashes=reuse_hashes\n        )\n\n    ###\n    # Parsing/collecting initial requirements\n    ###\n\n    constraints: list[InstallRequirement] = []\n    setup_file_found = False\n    for src_file in src_files:\n        is_setup_file = os.path.basename(src_file) in METADATA_FILENAMES\n        if not is_setup_file and build_deps_targets:\n            msg = (\n                \"--build-deps-for and --all-build-deps can be used only with the \"\n                \"setup.py, setup.cfg and pyproject.toml specs.\"\n            )\n            raise click.BadParameter(msg)\n\n        if src_file == \"-\":\n            # pip requires filenames and not files. Since we want to support\n            # piping from stdin, we need to briefly save the input from stdin\n            # to a temporary file and have pip read that.\n            with tempfile_compat.named_temp_file() as tmpfile:\n                tmpfile.write(sys.stdin.read())\n                tmpfile.flush()\n                reqs = list(\n                    parse_requirements(\n                        tmpfile.name,\n                        finder=repository.finder,\n                        session=repository.session,\n                        options=repository.options,\n                        comes_from_stdin=True,\n                    )\n                )\n            constraints.extend(reqs)\n        elif is_setup_file:\n            setup_file_found = True\n            try:\n                metadata = build_project_metadata(\n                    src_file=Path(src_file),\n                    build_targets=build_deps_targets,\n                    upgrade_packages=upgrade_packages,\n                    attempt_static_parse=not bool(build_deps_targets),\n                    isolated=build_isolation,\n                    quiet=log.verbosity <= 0,\n                )\n            except BuildBackendException as e:\n                log.error(str(e))\n                log.error(f\"Failed to parse {os.path.abspath(src_file)}\")\n                sys.exit(2)\n\n            if not only_build_deps:\n                constraints.extend(metadata.requirements)\n                if all_extras:\n                    extras += metadata.extras\n            if build_deps_targets:\n                assert isinstance(metadata, ProjectMetadata)\n                constraints.extend(metadata.build_requirements)\n        else:\n            constraints.extend(\n                parse_requirements(\n                    src_file,\n                    finder=repository.finder,\n                    session=repository.session,\n                    options=repository.options,\n                )\n            )\n\n    # Parse all constraints from `--constraint` files\n    for filename in constraint:\n        constraints.extend(\n            parse_requirements(\n                filename,\n                constraint=True,\n                finder=repository.finder,\n                options=repository.options,\n                session=repository.session,\n            )\n        )\n\n    if upgrade_packages:\n        with tempfile_compat.named_temp_file() as constraints_file:\n            constraints_file.write(\"\\n\".join(upgrade_packages))\n            constraints_file.flush()\n            reqs = list(\n                parse_requirements(\n                    constraints_file.name,\n                    finder=repository.finder,\n                    session=repository.session,\n                    options=repository.options,\n                    constraint=True,\n                )\n            )\n            for req in reqs:\n                req.comes_from = None\n        constraints.extend(reqs)\n\n    extras = tuple(itertools.chain.from_iterable(ex.split(\",\") for ex in extras))\n\n    if extras and not setup_file_found:\n        msg = \"--extra has effect only with setup.py and PEP-517 input formats\"\n        raise click.BadParameter(msg)\n\n    primary_packages = {\n        key_from_ireq(ireq) for ireq in constraints if not ireq.constraint\n    }\n\n    constraints.extend(\n        ireq for key, ireq in upgrade_install_reqs.items() if key in primary_packages\n    )\n\n    constraints = [req for req in constraints if req.match_markers(extras)]\n    for req in constraints:\n        drop_extras(req)\n\n    if repository.finder.index_urls:\n        log.debug(\"Using indexes:\")\n        with log.indentation():\n            for index_url in dedup(repository.finder.index_urls):\n                log.debug(redact_auth_from_url(index_url))\n    else:\n        log.debug(\"Ignoring indexes.\")\n\n    if repository.finder.find_links:\n        log.debug(\"\")\n        log.debug(\"Using links:\")\n        with log.indentation():\n            for find_link in dedup(repository.finder.find_links):\n                log.debug(redact_auth_from_url(find_link))\n\n    unsafe_package = tuple(canonicalize_name(pkg_name) for pkg_name in unsafe_package)\n\n    resolver_cls = LegacyResolver if resolver_name == \"legacy\" else BacktrackingResolver\n    try:\n        resolver = resolver_cls(\n            constraints=constraints,\n            existing_constraints=existing_pins,\n            repository=repository,\n            prereleases=(\n                pre or _pip_api.finder_allows_all_prereleases(repository.finder)\n            ),\n            cache=DependencyCache(cache_dir),\n            clear_caches=rebuild,\n            allow_unsafe=allow_unsafe,\n            unsafe_packages=set(unsafe_package),\n        )\n        results = resolver.resolve(max_rounds=max_rounds)\n        hashes = resolver.resolve_hashes(results) if generate_hashes else None\n    except NoCandidateFound as e:\n        if resolver_cls == LegacyResolver:  # pragma: no branch\n            log.error(\n                \"Using legacy resolver. \"\n                \"Consider using backtracking resolver with \"\n                \"`--resolver=backtracking`.\"\n            )\n\n        log.error(str(e))\n        sys.exit(2)\n    except PipToolsError as e:\n        log.error(str(e))\n        sys.exit(2)\n\n    log.debug(\"\")\n\n    linesep = _determine_linesep(\n        strategy=newline, filenames=(output_file.name, *src_files)\n    )\n\n    if strip_extras is None:\n        strip_extras = False\n        log.warning(\n            \"WARNING: --strip-extras is becoming the default \"\n            \"in version 8.0.0. To silence this warning, \"\n            \"either use --strip-extras to opt into the new default \"\n            \"or use --no-strip-extras to retain the existing behavior.\"\n        )\n\n    ##\n    # Output\n    ##\n\n    writer = OutputWriter(\n        _t.cast(_t.BinaryIO, output_file),\n        click_ctx=ctx,\n        dry_run=dry_run,\n        emit_header=header,\n        emit_index_url=emit_index_url,\n        emit_trusted_host=emit_trusted_host,\n        annotate=annotate,\n        annotation_style=annotation_style,\n        strip_extras=strip_extras,\n        generate_hashes=generate_hashes,\n        default_index_url=repository.DEFAULT_INDEX_URL,\n        index_urls=repository.finder.index_urls,\n        trusted_hosts=repository.finder.trusted_hosts,\n        format_control=repository.finder.format_control,\n        linesep=linesep,\n        allow_unsafe=allow_unsafe,\n        find_links=repository.finder.find_links,\n        emit_find_links=emit_find_links,\n        emit_options=emit_options,\n    )\n    writer.write(\n        results=results,\n        unsafe_packages=resolver.unsafe_packages,\n        unsafe_requirements=resolver.unsafe_constraints,\n        markers={\n            key_from_ireq(ireq): ireq.markers for ireq in constraints if ireq.markers\n        },\n        hashes=hashes,\n    )\n\n    if dry_run:\n        log.info(\"Dry-run, so nothing updated.\")\n"
  },
  {
    "path": "piptools/scripts/options.py",
    "content": "from __future__ import annotations\n\nimport typing as _t\n\nimport click\nfrom pip._internal.commands import create_command\nfrom pip._internal.utils.misc import redact_auth_from_url\n\nfrom piptools.locations import CACHE_DIR, DEFAULT_CONFIG_FILE_NAMES\nfrom piptools.utils import UNSAFE_PACKAGES, override_defaults_from_config_file\n\n_FC = _t.TypeVar(\"_FC\", bound=\"_t.Callable[..., _t.Any] | click.Command\")\n\nBuildTargetT = _t.Literal[\"sdist\", \"wheel\", \"editable\"]\nALL_BUILD_TARGETS: tuple[BuildTargetT, ...] = (\n    \"editable\",\n    \"sdist\",\n    \"wheel\",\n)\n\n\ndef help_option(*, epilog: str | None = None) -> _t.Callable[[_FC], _FC]:\n    \"\"\"A variant of the built-in click ``--help`` option, customized for pip-tools.\n\n    Unlike ``click.help_option``, this decorator accepts its own ``epilog`` text which\n    is printed *without indentation* after help text.\n    \"\"\"\n\n    def show_help(ctx: click.Context, param: click.Parameter, value: bool) -> None:\n        \"\"\"Callback that print the help page on ``<stdout>`` and exits.\"\"\"\n        if value and not ctx.resilient_parsing:\n            click.echo(ctx.get_help(), color=ctx.color)\n            if epilog is not None:\n                formatter = ctx.make_formatter()\n                formatter.write_text(epilog)\n                click.echo(\"\\n\" + formatter.getvalue().rstrip(\"\\n\"), color=ctx.color)\n            ctx.exit()\n\n    return click.option(  # type: ignore[return-value]\n        \"-h\",\n        \"--help\",\n        help=\"Show this message and exit.\",\n        callback=show_help,\n        is_eager=True,\n        expose_value=False,\n        is_flag=True,\n    )\n\n\ndef _get_default_option(option_name: str) -> _t.Any:\n    \"\"\"\n    Get default value of the pip's option (including option from pip.conf)\n    by a given option name.\n    \"\"\"\n    install_command = create_command(\"install\")\n    default_values = install_command.parser.get_default_values()\n    return getattr(default_values, option_name)\n\n\n# The options used by pip-compile and pip-sync are presented in no specific order.\n\nversion = click.version_option(package_name=\"pip-tools\")\n\ncolor = click.option(\n    \"--color/--no-color\",\n    default=None,\n    help=\"Force output to be colorized or not, instead of auto-detecting color support\",\n)\n\nverbose = click.option(\n    \"-v\",\n    \"--verbose\",\n    count=True,\n    help=\"Show more output\",\n)\nquiet = click.option(\n    \"-q\",\n    \"--quiet\",\n    count=True,\n    help=\"Give less output\",\n)\n\ndry_run = click.option(\n    \"-n\",\n    \"--dry-run\",\n    is_flag=True,\n    help=\"Only show what would happen, don't change anything\",\n)\n\npre = click.option(\n    \"-p\",\n    \"--pre\",\n    is_flag=True,\n    default=None,\n    help=\"Allow resolving to prereleases (default is not)\",\n)\n\nrebuild = click.option(\n    \"-r\",\n    \"--rebuild\",\n    is_flag=True,\n    help=\"Clear any caches upfront, rebuild from scratch\",\n)\n\nextra = click.option(\n    \"--extra\",\n    \"extras\",\n    multiple=True,\n    help=\"Name of an extras_require group to install; may be used more than once\",\n)\n\nall_extras = click.option(\n    \"--all-extras\",\n    is_flag=True,\n    default=False,\n    help=\"Install all extras_require groups\",\n)\n\nfind_links = click.option(\n    \"-f\",\n    \"--find-links\",\n    multiple=True,\n    help=\"Look for archives in this directory or on this HTML page; may be used more than once\",\n)\n\nindex_url = click.option(\n    \"-i\",\n    \"--index-url\",\n    help=\"Change index URL (defaults to {index_url})\".format(\n        index_url=redact_auth_from_url(_get_default_option(\"index_url\"))\n    ),\n)\n\nno_index = click.option(\n    \"--no-index\",\n    is_flag=True,\n    help=\"Ignore package index (only looking at --find-links URLs instead).\",\n)\n\nextra_index_url = click.option(\n    \"--extra-index-url\",\n    multiple=True,\n    help=\"Add another index URL to search; may be used more than once\",\n)\n\ncert = click.option(\"--cert\", help=\"Path to alternate CA bundle.\")\n\nclient_cert = click.option(\n    \"--client-cert\",\n    help=(\n        \"Path to SSL client certificate, a single file containing \"\n        \"the private key and the certificate in PEM format.\"\n    ),\n)\n\ntrusted_host = click.option(\n    \"--trusted-host\",\n    multiple=True,\n    help=(\n        \"Mark this host as trusted, even though it does not have \"\n        \"valid or any HTTPS; may be used more than once\"\n    ),\n)\n\nheader = click.option(\n    \"--header/--no-header\",\n    is_flag=True,\n    default=True,\n    help=\"Add header to generated file\",\n)\n\nemit_trusted_host = click.option(\n    \"--emit-trusted-host/--no-emit-trusted-host\",\n    is_flag=True,\n    default=True,\n    help=\"Add trusted host option to generated file\",\n)\n\nannotate = click.option(\n    \"--annotate/--no-annotate\",\n    is_flag=True,\n    default=True,\n    help=\"Annotate results, indicating where dependencies come from\",\n)\n\nannotation_style = click.option(\n    \"--annotation-style\",\n    type=click.Choice((\"line\", \"split\")),\n    default=\"split\",\n    help=\"Choose the format of annotation comments\",\n)\n\nupgrade = click.option(\n    \"-U\",\n    \"--upgrade/--no-upgrade\",\n    is_flag=True,\n    default=False,\n    help=\"Try to upgrade all dependencies to their latest versions\",\n)\n\nupgrade_package = click.option(\n    \"-P\",\n    \"--upgrade-package\",\n    \"upgrade_packages\",\n    nargs=1,\n    multiple=True,\n    help=\"Specify a particular package to upgrade; may be used more than once\",\n)\n\noutput_file = click.option(\n    \"-o\",\n    \"--output-file\",\n    nargs=1,\n    default=None,\n    type=click.File(\"w+b\", atomic=True, lazy=True),\n    help=(\n        \"Output file name. Required if more than one input file is given. \"\n        \"Will be derived from input file otherwise.\"\n    ),\n)\n\nnewline = click.option(\n    \"--newline\",\n    type=click.Choice((\"LF\", \"CRLF\", \"native\", \"preserve\"), case_sensitive=False),\n    default=\"preserve\",\n    help=\"Override the newline control characters used\",\n)\n\nallow_unsafe = click.option(\n    \"--allow-unsafe/--no-allow-unsafe\",\n    is_flag=True,\n    default=False,\n    help=(\n        \"Pin packages considered unsafe: {}.\\n\\n\"\n        \"WARNING: Future versions of pip-tools will enable this behavior by default. \"\n        \"Use --no-allow-unsafe to keep the old behavior. It is recommended to pass the \"\n        \"--allow-unsafe now to adapt to the upcoming change.\".format(\n            \", \".join(sorted(UNSAFE_PACKAGES))\n        )\n    ),\n)\n\nstrip_extras = click.option(\n    \"--strip-extras/--no-strip-extras\",\n    is_flag=True,\n    default=None,\n    help=\"Assure output file is constraints compatible, avoiding use of extras.\",\n)\n\ngenerate_hashes = click.option(\n    \"--generate-hashes\",\n    is_flag=True,\n    default=False,\n    help=\"Generate pip 8 style hashes in the resulting requirements file.\",\n)\n\nreuse_hashes = click.option(\n    \"--reuse-hashes/--no-reuse-hashes\",\n    is_flag=True,\n    default=True,\n    help=(\n        \"Improve the speed of --generate-hashes by reusing the hashes from an \"\n        \"existing output file.\"\n    ),\n)\n\nmax_rounds = click.option(\n    \"--max-rounds\",\n    default=10,\n    help=\"Maximum number of rounds before resolving the requirements aborts.\",\n)\n\nsrc_files = click.argument(\n    \"src_files\",\n    nargs=-1,\n    type=click.Path(exists=True, allow_dash=True),\n)\n\nbuild_isolation = click.option(\n    \"--build-isolation/--no-build-isolation\",\n    is_flag=True,\n    default=True,\n    help=(\n        \"Enable isolation when building a modern source distribution. \"\n        \"Build dependencies specified by PEP 518 must be already installed \"\n        \"if build isolation is disabled.\"\n    ),\n)\n\nemit_find_links = click.option(\n    \"--emit-find-links/--no-emit-find-links\",\n    is_flag=True,\n    default=True,\n    help=\"Add the find-links option to generated file\",\n)\n\ncache_dir = click.option(\n    \"--cache-dir\",\n    help=\"Store the cache data in DIRECTORY.\",\n    default=CACHE_DIR,\n    envvar=\"PIP_TOOLS_CACHE_DIR\",\n    show_default=True,\n    show_envvar=True,\n    type=click.Path(file_okay=False, writable=True),\n)\n\npip_args = click.option(\n    \"--pip-args\",\n    \"pip_args_str\",\n    help=\"Arguments to pass directly to the pip command.\",\n)\n\nresolver = click.option(\n    \"--resolver\",\n    \"resolver_name\",\n    type=click.Choice((\"legacy\", \"backtracking\")),\n    default=\"backtracking\",\n    envvar=\"PIP_TOOLS_RESOLVER\",\n    help=\"Choose the dependency resolver.\",\n)\n\nemit_index_url = click.option(\n    \"--emit-index-url/--no-emit-index-url\",\n    is_flag=True,\n    default=True,\n    help=\"Add index URL to generated file\",\n)\n\nemit_options = click.option(\n    \"--emit-options/--no-emit-options\",\n    is_flag=True,\n    default=True,\n    help=\"Add options to generated file\",\n)\n\nunsafe_package = click.option(\n    \"--unsafe-package\",\n    multiple=True,\n    help=(\n        \"Specify a package to consider unsafe; may be used more than once. \"\n        f\"Replaces default unsafe packages: {', '.join(sorted(UNSAFE_PACKAGES))}\"\n    ),\n)\n\nconfig = click.option(\n    \"--config\",\n    type=click.Path(\n        exists=True,\n        file_okay=True,\n        dir_okay=False,\n        readable=True,\n        allow_dash=False,\n        path_type=str,\n    ),\n    help=(\n        f\"Read configuration from TOML file. By default, looks for the following \"\n        f\"files in the given order: {', '.join(DEFAULT_CONFIG_FILE_NAMES)}.\"\n    ),\n    is_eager=True,\n    callback=override_defaults_from_config_file,\n)\n\nno_config = click.option(\n    \"--no-config\",\n    is_flag=True,\n    default=False,\n    help=\"Do not read any config file.\",\n    is_eager=True,\n)\n\nconstraint = click.option(\n    \"-c\",\n    \"--constraint\",\n    multiple=True,\n    help=\"Constrain versions using the given constraints file; may be used more than once.\",\n)\n\nask = click.option(\n    \"-a\",\n    \"--ask\",\n    is_flag=True,\n    help=\"Show what would happen, then ask whether to continue\",\n)\n\nforce = click.option(\n    \"--force\", is_flag=True, help=\"Proceed even if conflicts are found\"\n)\n\npython_executable = click.option(\n    \"--python-executable\",\n    help=\"Custom python executable path if targeting an environment other than current.\",\n)\n\nuser = click.option(\n    \"--user\",\n    \"user_only\",\n    is_flag=True,\n    help=\"Restrict attention to user directory\",\n)\n\nbuild_deps_for = click.option(\n    \"--build-deps-for\",\n    \"build_deps_targets\",\n    multiple=True,\n    type=click.Choice(ALL_BUILD_TARGETS),\n    help=\"Name of a build target to extract dependencies for. \"\n    \"Static dependencies declared in 'pyproject.toml::build-system.requires' will be included as \"\n    \"well; may be used more than once.\",\n)\n\nall_build_deps = click.option(\n    \"--all-build-deps\",\n    is_flag=True,\n    default=False,\n    help=\"Extract dependencies for all build targets. \"\n    \"Static dependencies declared in 'pyproject.toml::build-system.requires' will be included as \"\n    \"well.\",\n)\n\nonly_build_deps = click.option(\n    \"--only-build-deps\",\n    is_flag=True,\n    default=False,\n    help=\"Extract a package only if it is a build dependency.\",\n)\n"
  },
  {
    "path": "piptools/scripts/sync.py",
    "content": "from __future__ import annotations\n\nimport itertools\nimport os\nimport shlex\nimport shutil\nimport sys\nimport typing as _t\nfrom pathlib import Path\n\nimport click\nfrom pip._internal.commands import create_command\nfrom pip._internal.commands.install import InstallCommand\nfrom pip._internal.index.package_finder import PackageFinder\nfrom pip._internal.metadata import get_environment\n\nfrom .. import sync\nfrom .._compat import Distribution, parse_requirements\nfrom .._internal import _pip_api\nfrom ..exceptions import PipToolsError\nfrom ..logging import log\nfrom ..repositories import PyPIRepository\nfrom ..utils import (\n    flat_map,\n    get_required_pip_specification,\n    get_sys_path_for_python_executable,\n)\nfrom . import options\nfrom ._deprecations import filter_deprecated_pip_args\n\nDEFAULT_REQUIREMENTS_FILE = \"requirements.txt\"\n\nSYNC_EPILOG = \"\"\"\\b\nExamples:\n\\b\n    Synchronize environment with requirements.txt:\n    $ pip-sync\n\\b\n    Synchronize with multiple requirements files:\n    $ pip-sync requirements.txt dev.txt\n\\b\n    Preview what would be installed or uninstalled:\n    $ pip-sync --dry-run\n\\b\n    Ask for confirmation before making changes:\n    $ pip-sync --ask\n\"\"\"\n\n\n@click.command(name=\"pip-sync\")\n@options.help_option(epilog=SYNC_EPILOG)\n@options.version\n@options.ask\n@options.dry_run\n@options.force\n@options.find_links\n@options.index_url\n@options.extra_index_url\n@options.trusted_host\n@options.no_index\n@options.python_executable\n@options.verbose\n@options.quiet\n@options.user\n@options.cert\n@options.client_cert\n@options.src_files\n@options.pip_args\n@options.config\n@options.no_config\ndef cli(\n    ask: bool,\n    dry_run: bool,\n    force: bool,\n    find_links: tuple[str, ...],\n    index_url: str | None,\n    extra_index_url: tuple[str, ...],\n    trusted_host: tuple[str, ...],\n    no_index: bool,\n    python_executable: str | None,\n    verbose: int,\n    quiet: int,\n    user_only: bool,\n    cert: str | None,\n    client_cert: str | None,\n    src_files: tuple[str, ...],\n    pip_args_str: str | None,\n    config: Path | None,\n    no_config: bool,\n) -> None:\n    \"\"\"Synchronize virtual environment with requirements.txt.\"\"\"\n    log.verbosity = verbose - quiet\n\n    if not src_files:\n        if os.path.exists(DEFAULT_REQUIREMENTS_FILE):\n            src_files = (DEFAULT_REQUIREMENTS_FILE,)\n        else:\n            msg = \"No requirement files given and no {} found in the current directory\"\n            log.error(msg.format(DEFAULT_REQUIREMENTS_FILE))\n            sys.exit(2)\n\n    if any(src_file.endswith(\".in\") for src_file in src_files):\n        msg = (\n            \"Some input files have the .in extension, which is most likely an error \"\n            \"and can cause weird behaviour. You probably meant to use \"\n            \"the corresponding *.txt file?\"\n        )\n        if force:\n            log.warning(\"WARNING: \" + msg)\n        else:\n            log.error(\"ERROR: \" + msg)\n            sys.exit(2)\n\n    if config:\n        log.debug(f\"Using pip-tools configuration defaults found in '{config !s}'.\")\n\n    if python_executable:\n        _validate_python_executable(python_executable)\n\n    install_command = _t.cast(InstallCommand, create_command(\"install\"))\n    options, _ = install_command.parse_args([])\n    session = install_command._build_session(options)\n    finder = install_command._build_package_finder(options=options, session=session)\n\n    # Parse requirements file. Note, all options inside requirements file\n    # will be collected by the finder.\n    requirements = flat_map(\n        lambda src: parse_requirements(src, finder=finder, session=session), src_files\n    )\n\n    try:\n        merged_requirements = sync.merge(requirements, ignore_conflicts=force)\n    except PipToolsError as e:\n        log.error(str(e))\n        sys.exit(2)\n\n    paths = (\n        None\n        if python_executable is None\n        else get_sys_path_for_python_executable(python_executable)\n    )\n    installed_dists = _get_installed_distributions(\n        user_only=user_only,\n        local_only=python_executable is None,\n        paths=paths,\n    )\n    to_install, to_uninstall = sync.diff(merged_requirements, installed_dists)\n\n    install_flags = _compose_install_flags(\n        finder,\n        no_index=no_index,\n        index_url=index_url,\n        extra_index_url=extra_index_url,\n        trusted_host=trusted_host,\n        find_links=find_links,\n        user_only=user_only,\n        cert=cert,\n        client_cert=client_cert,\n    ) + shlex.split(pip_args_str or \"\")\n    install_flags = filter_deprecated_pip_args(install_flags)\n\n    sys.exit(\n        sync.sync(\n            to_install,\n            to_uninstall,\n            dry_run=dry_run,\n            install_flags=install_flags,\n            ask=ask,\n            python_executable=python_executable,\n        )\n    )\n\n\ndef _validate_python_executable(python_executable: str) -> None:\n    \"\"\"\n    Validates incoming python_executable argument passed to CLI.\n    \"\"\"\n    resolved_python_executable = shutil.which(python_executable)\n    if resolved_python_executable is None:\n        msg = \"Could not resolve '{}' as valid executable path or alias.\"\n        log.error(msg.format(python_executable))\n        sys.exit(2)\n\n    # Ensure that target python executable has the right version of pip installed\n    pip_version = _pip_api.get_pip_version_for_python_executable(python_executable)\n    required_pip_specification = get_required_pip_specification()\n    if not required_pip_specification.contains(pip_version, prereleases=True):\n        msg = (\n            \"Target python executable '{}' has pip version {} installed. \"\n            \"Version {} is expected.\"\n        )\n        log.error(\n            msg.format(python_executable, pip_version, required_pip_specification)\n        )\n        sys.exit(2)\n\n\ndef _compose_install_flags(\n    finder: PackageFinder,\n    no_index: bool,\n    index_url: str | None,\n    extra_index_url: tuple[str, ...],\n    trusted_host: tuple[str, ...],\n    find_links: tuple[str, ...],\n    user_only: bool,\n    cert: str | None,\n    client_cert: str | None,\n) -> list[str]:\n    \"\"\"\n    Compose install flags with the given finder and CLI options.\n    \"\"\"\n    result = []\n\n    # Build --index-url/--extra-index-url/--no-index\n    if no_index:\n        result.append(\"--no-index\")\n    elif index_url is not None:\n        result.extend([\"--index-url\", index_url])\n    elif finder.index_urls:\n        finder_index_url = finder.index_urls[0]\n        if finder_index_url != PyPIRepository.DEFAULT_INDEX_URL:\n            result.extend([\"--index-url\", finder_index_url])\n        for extra_index in finder.index_urls[1:]:\n            result.extend([\"--extra-index-url\", extra_index])\n    else:\n        result.append(\"--no-index\")\n\n    for extra_index in extra_index_url:\n        result.extend([\"--extra-index-url\", extra_index])\n\n    # Build --trusted-hosts\n    for host in itertools.chain(trusted_host, finder.trusted_hosts):\n        result.extend([\"--trusted-host\", host])\n\n    # Build --find-links\n    for link in itertools.chain(find_links, finder.find_links):\n        result.extend([\"--find-links\", link])\n\n    # Build format controls --no-binary/--only-binary\n    for format_control in (\"no_binary\", \"only_binary\"):\n        formats = getattr(finder.format_control, format_control)\n        if not formats:\n            continue\n        result.extend(\n            [\"--\" + format_control.replace(\"_\", \"-\"), \",\".join(sorted(formats))]\n        )\n\n    if user_only:\n        result.append(\"--user\")\n\n    if cert is not None:\n        result.extend([\"--cert\", cert])\n\n    if client_cert is not None:\n        result.extend([\"--client-cert\", client_cert])\n\n    return result\n\n\ndef _get_installed_distributions(\n    local_only: bool = True,\n    user_only: bool = False,\n    paths: list[str] | None = None,\n) -> list[Distribution]:\n    \"\"\"Return a list of installed Distribution objects.\"\"\"\n\n    env = get_environment(paths)\n    dists = env.iter_installed_distributions(\n        local_only=local_only,\n        user_only=user_only,\n        skip=[],\n    )\n    return [Distribution.from_pip_distribution(dist) for dist in dists]\n"
  },
  {
    "path": "piptools/sync.py",
    "content": "from __future__ import annotations\n\nimport collections\nimport os\nimport sys\nimport tempfile\nfrom collections.abc import Iterable, Mapping, ValuesView\nfrom subprocess import run  # nosec\n\nimport click\nfrom pip._internal.models.direct_url import ArchiveInfo\nfrom pip._internal.req import InstallRequirement\nfrom pip._internal.utils.compat import stdlib_pkgs\nfrom pip._internal.utils.direct_url_helpers import (\n    direct_url_as_pep440_direct_reference,\n    direct_url_from_link,\n)\n\nfrom ._compat import Distribution, canonicalize_name, get_dev_pkgs\nfrom .exceptions import IncompatibleRequirements\nfrom .logging import log\nfrom .utils import (\n    flat_map,\n    format_requirement,\n    get_hashes_from_ireq,\n    is_url_requirement,\n    key_from_ireq,\n    key_from_req,\n)\n\nPACKAGES_TO_IGNORE = [\n    \"-markerlib\",\n    \"pip\",\n    \"pip-tools\",\n    \"pip-review\",\n    \"pkg-resources\",\n    *stdlib_pkgs,\n    *get_dev_pkgs(),\n]\n\n\ndef dependency_tree(\n    installed_keys: Mapping[str, Distribution], root_key: str\n) -> set[str]:\n    \"\"\"Calculate the dependency tree for a package.\n\n    Return a collection of all of the package's dependencies.\n    Uses a DFS traversal algorithm.\n\n    ``installed_keys`` should be a {key: requirement} mapping, e.g.\n    {'django': from_line('django==1.8')}\n    :param root_key: the key to return the dependency tree for\n    :type root_key: str\n    \"\"\"\n    dependencies = set()\n    queue: collections.deque[Distribution] = collections.deque()\n\n    if root_key in installed_keys:\n        dep = installed_keys[root_key]\n        queue.append(dep)\n\n    while queue:\n        v = queue.popleft()\n        key = canonicalize_name(v.key)\n        if key in dependencies:\n            continue\n\n        dependencies.add(key)\n\n        for dep_specifier in v.requires:\n            dep_name = key_from_req(dep_specifier)\n            if dep_name in installed_keys:\n                dep = installed_keys[dep_name]\n\n                if dep_specifier.specifier.contains(dep.version):\n                    queue.append(dep)\n\n    return dependencies\n\n\ndef get_dists_to_ignore(installed: Iterable[Distribution]) -> list[str]:\n    \"\"\"Return a collection of package names to ignore by ``pip-sync``.\n\n    Based on the currently installed environment.  For example, when pip-tools\n    is installed in the local environment, it should be ignored, including all\n    of its dependencies (e.g. click).  When pip-tools is not installed\n    locally, click should also be installed/uninstalled depending on the given\n    requirements.\n    \"\"\"\n    installed_keys = {canonicalize_name(r.key): r for r in installed}\n    return list(\n        flat_map(lambda req: dependency_tree(installed_keys, req), PACKAGES_TO_IGNORE)\n    )\n\n\ndef merge(\n    requirements: Iterable[InstallRequirement], ignore_conflicts: bool\n) -> ValuesView[InstallRequirement]:\n    by_key: dict[str, InstallRequirement] = {}\n\n    for ireq in requirements:\n        # Limitation: URL requirements are merged by precise string match, so\n        # \"file:///example.zip#egg=example\", \"file:///example.zip\", and\n        # \"example==1.0\" will not merge with each other\n        if ireq.match_markers():\n            key = key_from_ireq(ireq)\n\n            if not ignore_conflicts:\n                existing_ireq = by_key.get(key)\n                if existing_ireq:\n                    # NOTE: We check equality here since we can assume that the\n                    # requirements are all pinned\n                    if (\n                        ireq.req\n                        and existing_ireq.req\n                        and ireq.specifier != existing_ireq.specifier\n                    ):\n                        raise IncompatibleRequirements(ireq, existing_ireq)\n\n            # TODO: Always pick the largest specifier in case of a conflict\n            by_key[key] = ireq\n    return by_key.values()\n\n\ndef diff_key_from_ireq(ireq: InstallRequirement) -> str:\n    \"\"\"Calculate key for comparing a compiled requirement with installed modules.\n\n    For URL requirements, only provide a useful key if the url includes\n    a hash, e.g. #sha1=..., in any of the supported hash algorithms.\n    Otherwise return ``ireq.link`` so the key will not match and the package will\n    reinstall. Reinstall is necessary to ensure that packages will reinstall\n    if the contents at the URL have changed but the version has not.\n    \"\"\"\n    if is_url_requirement(ireq):\n        if getattr(ireq.req, \"name\", None) and ireq.link.has_hash:\n            return str(\n                direct_url_as_pep440_direct_reference(\n                    direct_url_from_link(ireq.link), ireq.req.name\n                )\n            )\n        # TODO: Also support VCS and editable installs.\n        return str(ireq.link)\n    return key_from_ireq(ireq)\n\n\ndef diff_key_from_req(req: Distribution) -> str:\n    \"\"\"Get a unique key for the requirement.\"\"\"\n    key = canonicalize_name(req.key)\n    if (\n        req.direct_url\n        and isinstance(req.direct_url.info, ArchiveInfo)\n        and req.direct_url.info.hash\n    ):\n        key = direct_url_as_pep440_direct_reference(req.direct_url, key)\n    # TODO: Also support VCS and editable installs.\n    return key\n\n\ndef diff(\n    compiled_requirements: Iterable[InstallRequirement],\n    installed_dists: Iterable[Distribution],\n) -> tuple[set[InstallRequirement], set[str]]:\n    \"\"\"Calculate which packages should be installed or uninstalled.\n\n    Compared are the compiled requirements and a list of currently\n    installed modules.\n    \"\"\"\n    requirements_lut = {diff_key_from_ireq(r): r for r in compiled_requirements}\n\n    satisfied = set()  # holds keys\n    to_install = set()  # holds InstallRequirement objects\n    to_uninstall = set()  # holds keys\n\n    pkgs_to_ignore = get_dists_to_ignore(installed_dists)\n    for dist in installed_dists:\n        key = diff_key_from_req(dist)\n        if key not in requirements_lut or not requirements_lut[key].match_markers():\n            to_uninstall.add(key)\n        elif requirements_lut[key].specifier.contains(dist.version):\n            satisfied.add(key)\n\n    for key, requirement in requirements_lut.items():\n        if key not in satisfied and requirement.match_markers():\n            to_install.add(requirement)\n\n    # Make sure to not uninstall any packages that should be ignored\n    to_uninstall -= set(pkgs_to_ignore)\n\n    return (to_install, to_uninstall)\n\n\ndef sync(\n    to_install: Iterable[InstallRequirement],\n    to_uninstall: Iterable[InstallRequirement],\n    dry_run: bool = False,\n    install_flags: list[str] | None = None,\n    ask: bool = False,\n    python_executable: str | None = None,\n) -> int:\n    \"\"\"Install and uninstall the given sets of modules.\"\"\"\n    exit_code = 0\n\n    python_executable = python_executable or sys.executable\n\n    if not to_uninstall and not to_install:\n        log.info(\"Everything up-to-date\", err=False)\n        return exit_code\n\n    pip_flags = []\n    if log.verbosity < 0:\n        pip_flags += [\"-q\"]\n\n    if ask:\n        dry_run = True\n\n    if dry_run:\n        if to_uninstall:\n            click.echo(\"Would uninstall:\")\n            for pkg in sorted(to_uninstall):\n                click.echo(f\"  {pkg}\")\n\n        if to_install:\n            click.echo(\"Would install:\")\n            for ireq in sorted(to_install, key=key_from_ireq):\n                click.echo(f\"  {format_requirement(ireq)}\")\n\n        exit_code = 1\n\n    if ask and click.confirm(\"Would you like to proceed with these changes?\"):\n        dry_run = False\n        exit_code = 0\n\n    if not dry_run:\n        if to_uninstall:\n            run(  # nosec\n                [\n                    python_executable,\n                    \"-m\",\n                    \"pip\",\n                    \"uninstall\",\n                    \"-y\",\n                    *pip_flags,\n                    *sorted(to_uninstall),\n                ],\n                check=True,\n            )\n\n        if to_install:\n            if install_flags is None:\n                install_flags = []\n            # prepare requirement lines\n            req_lines = []\n            for ireq in sorted(to_install, key=key_from_ireq):\n                ireq_hashes = get_hashes_from_ireq(ireq)\n                req_lines.append(format_requirement(ireq, hashes=ireq_hashes))\n\n            # save requirement lines to a temporary file\n            tmp_req_file = tempfile.NamedTemporaryFile(mode=\"wt\", delete=False)\n            tmp_req_file.write(\"\\n\".join(req_lines))\n            tmp_req_file.close()\n\n            try:\n                run(  # nosec\n                    [\n                        python_executable,\n                        \"-m\",\n                        \"pip\",\n                        \"install\",\n                        \"-r\",\n                        tmp_req_file.name,\n                        *pip_flags,\n                        *install_flags,\n                    ],\n                    check=True,\n                )\n            finally:\n                os.unlink(tmp_req_file.name)\n\n    return exit_code\n"
  },
  {
    "path": "piptools/utils.py",
    "content": "from __future__ import annotations\n\nimport collections\nimport copy\nimport difflib\nimport itertools\nimport json\nimport os\nimport re\nimport shlex\nimport typing as _t\nfrom collections.abc import Iterable, Iterator\nfrom pathlib import Path\n\nimport click\nfrom click.core import ParameterSource\nfrom click.utils import LazyFile\nfrom pip._internal.req import InstallRequirement\nfrom pip._internal.resolution.resolvelib.base import Requirement as PipRequirement\nfrom pip._internal.utils.misc import redact_auth_from_url\nfrom pip._internal.vcs import is_url\nfrom pip._vendor.packaging.markers import Marker\nfrom pip._vendor.packaging.requirements import Requirement\nfrom pip._vendor.packaging.specifiers import SpecifierSet\nfrom pip._vendor.pkg_resources import get_distribution\n\nfrom piptools.locations import DEFAULT_CONFIG_FILE_NAMES\n\nfrom ._compat import _tomllib_compat, canonicalize_name\nfrom ._internal import _subprocess\n\n_KT = _t.TypeVar(\"_KT\")\n_VT = _t.TypeVar(\"_VT\")\n_T = _t.TypeVar(\"_T\")\n_S = _t.TypeVar(\"_S\")\n\nUNSAFE_PACKAGES = {\"setuptools\", \"distribute\", \"pip\"}\nCOMPILE_EXCLUDE_OPTIONS = {\n    \"--dry-run\",\n    \"--quiet\",\n    \"--rebuild\",\n    \"--upgrade\",\n    \"--upgrade-package\",\n    \"--verbose\",\n    \"--cache-dir\",\n    \"--no-reuse-hashes\",\n    \"--no-config\",\n}\n\n# Set of option that are only negative, i.e. --no-<option>\nONLY_NEGATIVE_OPTIONS = {\"--no-index\"}\n\n\ndef key_from_ireq(ireq: InstallRequirement) -> str:\n    \"\"\"Get a standardized key for an InstallRequirement.\"\"\"\n    if ireq.req is None and ireq.link is not None:\n        return str(ireq.link)\n    else:\n        return key_from_req(ireq.req)\n\n\ndef key_from_req(req: InstallRequirement | Requirement | PipRequirement) -> str:\n    \"\"\"\n    Get an all-lowercase version of the requirement's name.\n\n    **Note:** If the argument is an instance of\n    ``pip._internal.resolution.resolvelib.base.Requirement`` (like\n    ``pip._internal.resolution.resolvelib.requirements.SpecifierRequirement``),\n    then the name might include an extras specification.\n    Apply :py:func:`strip_extras` to the result of this function if you need\n    the package name only.\n\n    :param req: the requirement the key is computed for\n    :return: the canonical name of the requirement\n    \"\"\"\n    return canonicalize_name(req.name)\n\n\ndef comment(text: str) -> str:\n    return click.style(text, fg=\"green\")\n\n\ndef is_url_requirement(ireq: InstallRequirement) -> bool:\n    \"\"\"\n    Return :py:data:`True` if requirement was specified as a path or URL.\n\n    ``ireq.original_link`` will have been set by ``InstallRequirement.__init__``\n    \"\"\"\n    return bool(ireq.original_link)\n\n\ndef format_requirement(\n    ireq: InstallRequirement,\n    marker: Marker | None = None,\n    hashes: set[str] | None = None,\n) -> str:\n    \"\"\"\n    Generic formatter for pretty printing InstallRequirements to the terminal\n    in a less verbose way than using its ``__str__`` method.\n    \"\"\"\n    if ireq.editable:\n        line = f\"-e {ireq.link.url}\"\n    elif is_url_requirement(ireq):\n        line = _build_direct_reference_best_efforts(ireq)\n    else:\n        # Canonicalize the requirement name\n        # https://packaging.pypa.io/en/latest/utils.html#packaging.utils.canonicalize_name\n        req = copy.copy(ireq.req)\n        req.name = canonicalize_name(req.name)\n        line = str(req)\n\n    if marker:\n        line = f\"{line} ; {marker}\"\n\n    if hashes:\n        for hash_ in sorted(hashes):\n            line += f\" \\\\\\n    --hash={hash_}\"\n\n    return line\n\n\ndef _build_direct_reference_best_efforts(ireq: InstallRequirement) -> str:\n    \"\"\"\n    Return a string of a direct reference URI, whenever possible.\n\n    See https://www.python.org/dev/peps/pep-0508/\n    \"\"\"\n    # If the requirement has no name then we cannot build a direct reference.\n    if not ireq.name:\n        return _t.cast(str, ireq.link.url)\n\n    # Look for a relative file path, the direct reference currently does not work with it.\n    if ireq.link.is_file and not ireq.link.path.startswith(\"/\"):\n        return _t.cast(str, ireq.link.url)\n\n    # If we get here then we have a requirement that supports direct reference.\n    # We need to remove the egg if it exists and keep the rest of the fragments.\n    lowered_ireq_name = canonicalize_name(ireq.name)\n    extras = f\"[{','.join(sorted(ireq.extras))}]\" if ireq.extras else \"\"\n    direct_reference = f\"{lowered_ireq_name}{extras} @ {ireq.link.url_without_fragment}\"\n    fragments = []\n\n    # Check if there is any fragment to add to the URI.\n    if ireq.link.subdirectory_fragment:\n        fragments.append(f\"subdirectory={ireq.link.subdirectory_fragment}\")\n\n    if ireq.link.has_hash:\n        fragments.append(f\"{ireq.link.hash_name}={ireq.link.hash}\")\n\n    # Then add the fragments into the URI, if any.\n    if fragments:\n        direct_reference += f\"#{'&'.join(fragments)}\"\n\n    return direct_reference\n\n\ndef format_specifier(ireq: InstallRequirement) -> str:\n    \"\"\"\n    Generic formatter for pretty printing the specifier part of\n    InstallRequirements to the terminal.\n    \"\"\"\n    # TODO: Ideally, this is carried over to the pip library itself\n    specs = ireq.specifier if ireq.req is not None else SpecifierSet()\n    # FIXME: remove ignore type marker once the following issue get fixed\n    #        https://github.com/python/mypy/issues/9656\n    specs = sorted(specs, key=lambda x: x.version)\n    return \",\".join(str(s) for s in specs) or \"<any>\"\n\n\ndef is_pinned_requirement(ireq: InstallRequirement) -> bool:\n    \"\"\"\n    Return whether an InstallRequirement is a \"pinned\" requirement.\n\n    An InstallRequirement is considered pinned if:\n\n    - Is not editable\n    - It has exactly one specifier\n    - That specifier is \"==\"\n    - The version does not contain a wildcard\n\n    Examples:\n        django==1.8   # pinned\n        django>1.8    # NOT pinned\n        django~=1.8   # NOT pinned\n        django==1.*   # NOT pinned\n    \"\"\"\n    if ireq.editable:\n        return False\n\n    if ireq.req is None or len(ireq.specifier) != 1:\n        return False\n\n    spec = next(iter(ireq.specifier))\n    return spec.operator in {\"==\", \"===\"} and not spec.version.endswith(\".*\")\n\n\ndef as_tuple(ireq: InstallRequirement) -> tuple[str, str, tuple[str, ...]]:\n    \"\"\"\n    Pull out the (name: str, version:str, extras:(str)) tuple from\n    the pinned InstallRequirement.\n    \"\"\"\n    if not is_pinned_requirement(ireq):\n        raise TypeError(f\"Expected a pinned InstallRequirement, got {ireq}\")\n\n    name = key_from_ireq(ireq)\n    version = next(iter(ireq.specifier)).version\n    extras = tuple(sorted(ireq.extras))\n    return name, version, extras\n\n\ndef flat_map(\n    fn: _t.Callable[[_T], Iterable[_S]], collection: Iterable[_T]\n) -> Iterator[_S]:\n    \"\"\"Map a function over a collection and flatten the result by one-level\"\"\"\n    return itertools.chain.from_iterable(map(fn, collection))\n\n\ndef lookup_table_from_tuples(values: Iterable[tuple[_KT, _VT]]) -> dict[_KT, set[_VT]]:\n    \"\"\"Build a dict-based lookup table (index) elegantly.\"\"\"\n    lut: dict[_KT, set[_VT]] = collections.defaultdict(set)\n    for k, v in values:\n        lut[k].add(v)\n    return dict(lut)\n\n\ndef lookup_table(\n    values: Iterable[_VT], key: _t.Callable[[_VT], _KT]\n) -> dict[_KT, set[_VT]]:\n    \"\"\"Build a dict-based lookup table (index) elegantly.\"\"\"\n    return lookup_table_from_tuples((key(v), v) for v in values)\n\n\ndef dedup(iterable: Iterable[_T]) -> Iterable[_T]:\n    \"\"\"\n    Deduplicate an iterable object like ``iter(set(iterable))`` but\n    order-preserved.\n    \"\"\"\n    return iter(dict.fromkeys(iterable))\n\n\ndef drop_extras(ireq: InstallRequirement) -> None:\n    \"\"\"Remove \"extra\" markers (PEP-508) from requirement.\"\"\"\n    if ireq.markers is None:\n        return\n    ireq.markers._markers = _drop_extras(ireq.markers._markers)\n    if not ireq.markers._markers:\n        ireq.markers = None\n\n\ndef _drop_extras(markers: list[_T]) -> list[_T]:\n    # drop `extra` tokens\n    to_remove: list[int] = []\n    for i, token in enumerate(markers):\n        # operator (and/or)\n        if isinstance(token, str):\n            continue\n        # sub-expression (inside braces)\n        if isinstance(token, list):\n            markers[i] = _drop_extras(token)  # type: ignore\n            if markers[i]:\n                continue\n            to_remove.append(i)\n            continue\n        # test expression (like `extra == \"dev\"`)\n        assert isinstance(token, tuple)\n        if token[0].value == \"extra\":\n            to_remove.append(i)\n    for i in reversed(to_remove):\n        markers.pop(i)\n\n    # drop duplicate bool operators (and/or)\n    to_remove = []\n    for i, (token1, token2) in enumerate(zip(markers, markers[1:])):\n        if not isinstance(token1, str):\n            continue\n        if not isinstance(token2, str):\n            continue\n        if token1 == \"and\":\n            to_remove.append(i)\n        else:\n            to_remove.append(i + 1)\n    for i in reversed(to_remove):\n        markers.pop(i)\n    if markers and isinstance(markers[0], str):\n        markers.pop(0)\n    if markers and isinstance(markers[-1], str):\n        markers.pop(-1)\n\n    return markers\n\n\ndef get_hashes_from_ireq(ireq: InstallRequirement) -> set[str]:\n    \"\"\"\n    Given an InstallRequirement, return a set of string hashes in the format\n    \"{algorithm}:{hash}\". Return an empty set if there are no hashes in the\n    requirement options.\n    \"\"\"\n    result = set()\n    for algorithm, hexdigests in ireq.hash_options.items():\n        for hash_ in hexdigests:\n            result.add(f\"{algorithm}:{hash_}\")\n    return result\n\n\ndef get_compile_command(click_ctx: click.Context) -> str:\n    \"\"\"\n    Return a normalized compile command depending on cli context.\n\n    The command will be normalized by:\n        - expanding options short to long\n        - removing values that are already default\n        - sorting the arguments\n        - removing one-off arguments like '--upgrade'\n        - removing arguments that don't change build behaviour like '--verbose'\n    \"\"\"\n    from piptools.scripts.compile import cli\n\n    # Map of the compile cli options (option name -> click.Option)\n    compile_options = {option.name: option for option in cli.params}\n\n    left_args = []\n    right_args = []\n\n    for option_name, value in click_ctx.params.items():\n        option = compile_options[option_name]\n\n        # Collect variadic args separately, they will be added\n        # at the end of the command later\n        if option.nargs < 0:\n            # These will necessarily be src_files\n            # Re-add click-stripped '--' if any start with '-'\n            if any(val.startswith(\"-\") and val != \"-\" for val in value):\n                right_args.append(\"--\")\n            right_args.extend([shlex.quote(val) for val in value])\n            continue\n\n        assert isinstance(option, click.Option)\n\n        # Get the latest option name (usually it'll be a long name)\n        option_long_name = option.opts[-1]\n\n        negative_option = None\n        if option.is_flag and option.secondary_opts:\n            # get inverse flag --no-{option_long_name}\n            negative_option = option.secondary_opts[-1]\n\n        # Exclude one-off options (--upgrade/--upgrade-package/--rebuild/...)\n        # or options that don't change compile behaviour (--verbose/--dry-run/...)\n        if {option_long_name, negative_option} & COMPILE_EXCLUDE_OPTIONS:\n            continue\n\n        # Exclude config option if it's the default one\n        if option_long_name == \"--config\":\n            parameter_source = click_ctx.get_parameter_source(option_name)\n            if (\n                str(value) in DEFAULT_CONFIG_FILE_NAMES\n                or parameter_source == ParameterSource.DEFAULT\n            ):\n                continue\n\n        # Skip options without a value\n        if value is None:\n            continue\n\n        # Skip options with a default value\n        if option.default == value:\n            continue\n\n        # Use a file name for file-like objects\n        if isinstance(value, LazyFile):\n            value = value.name\n\n        # Convert value to the list\n        if not isinstance(value, (tuple, list)):\n            value = [value]\n\n        for val in value:\n            # Flags don't have a value, thus add to args true or false option long name\n            if option.is_flag:\n                # If there are false-options, choose an option name depending on a value\n                if option.secondary_opts:\n                    # Get the latest false-option\n                    secondary_option_long_name = option.secondary_opts[-1]\n                    arg = option_long_name if val else secondary_option_long_name\n                # There are no false-options, use true-option\n                else:\n                    arg = option_long_name\n                left_args.append(shlex.quote(arg))\n            # Append to args the option with a value\n            else:\n                if isinstance(val, str) and is_url(val):\n                    val = redact_auth_from_url(val)\n                if option.name == \"pip_args_str\":\n                    # shlex.quote() would produce functional but noisily quoted results,\n                    # e.g. --pip-args='--cache-dir='\"'\"'/tmp/with spaces'\"'\"''\n                    # Instead, we try to get more legible quoting via repr:\n                    left_args.append(f\"{option_long_name}={repr(val)}\")\n                else:\n                    left_args.append(f\"{option_long_name}={shlex.quote(str(val))}\")\n\n    return \" \".join([\"pip-compile\", *sorted(left_args), *sorted(right_args)])\n\n\ndef get_required_pip_specification() -> SpecifierSet:\n    \"\"\"\n    Return pip version specifier requested by current pip-tools installation.\n    \"\"\"\n    project_dist = get_distribution(\"pip-tools\")\n    requirement = next(\n        (r for r in project_dist.requires() if r.name == \"pip\"), None\n    )  # pragma: no branch\n    assert (\n        requirement is not None\n    ), \"'pip' is expected to be in the list of pip-tools requirements\"\n    return requirement.specifier\n\n\ndef get_sys_path_for_python_executable(python_executable: str) -> list[str]:\n    \"\"\"\n    Return sys.path list for the given python executable.\n    \"\"\"\n    result = _subprocess.run_python_snippet(\n        python_executable, \"import sys;import json;print(json.dumps(sys.path))\"\n    )\n\n    paths = json.loads(result)\n    assert isinstance(paths, list)\n    assert all(isinstance(i, str) for i in paths)\n    return [os.path.abspath(path) for path in paths]\n\n\ndef omit_list_value(lst: list[_T], value: _T) -> list[_T]:\n    \"\"\"Produce a new list with a given value skipped.\"\"\"\n    return [item for item in lst if item != value]\n\n\n_strip_extras_re = re.compile(r\"\\[.+?\\]\")\n\n\ndef strip_extras(name: str) -> str:\n    \"\"\"Strip extras from package name, e.g. pytest[testing] -> pytest.\"\"\"\n    return re.sub(_strip_extras_re, \"\", name)\n\n\ndef override_defaults_from_config_file(\n    ctx: click.Context, param: click.Parameter, value: str | None\n) -> Path | None:\n    \"\"\"\n    Override ``click.Command`` defaults based on specified or discovered config\n    file, returning the ``pathlib.Path`` of that config file if specified or\n    discovered.\n\n    :returns: :py:data:`None` if no such file is found, else returns the path.\n\n    ``pip-tools`` will use the first config file found, searching in this order:\n    an explicitly given config file, a ``.pip-tools.toml``, a ``pyproject.toml``\n    file. Those files are searched for in the same directory as the requirements\n    input file, or the current working directory if requirements come via stdin.\n    \"\"\"\n    if ctx.params.get(\"no_config\"):\n        return None\n\n    if value is None:\n        config_file = select_config_file(ctx.params.get(\"src_files\", ()))\n        if config_file is None:\n            return None\n    else:\n        config_file = Path(value)\n\n    config = parse_config_file(ctx, config_file)\n\n    _validate_config(ctx, config)\n    _assign_config_to_cli_context(ctx, config)\n\n    return config_file\n\n\ndef _assign_config_to_cli_context(\n    click_context: click.Context,\n    cli_config_mapping: dict[str, _t.Any],\n) -> None:\n    if click_context.default_map is None:\n        click_context.default_map = {}\n\n    click_context.default_map.update(cli_config_mapping)\n\n\ndef _validate_config(\n    click_context: click.Context,\n    config: dict[str, _t.Any],\n) -> None:\n    \"\"\"\n    Validate parsed config against click command params.\n\n    :raises click.NoSuchOption: if config contains unknown keys.\n    :raises click.BadOptionUsage: if config contains invalid values.\n    \"\"\"\n    from piptools.scripts.compile import cli as compile_cli\n    from piptools.scripts.sync import cli as sync_cli\n\n    compile_cli_params = {\n        param.name: param for param in compile_cli.params if param.name is not None\n    }\n\n    sync_cli_params = {\n        param.name: param for param in sync_cli.params if param.name is not None\n    }\n\n    all_keys = set(compile_cli_params) | set(sync_cli_params)\n\n    for key, value in config.items():\n        # Validate unknown keys in both compile and sync\n        if key not in all_keys:\n            possibilities = difflib.get_close_matches(key, all_keys)\n            raise click.NoSuchOption(\n                option_name=key,\n                message=f\"No such config key {key!r}.\",\n                possibilities=possibilities,\n                ctx=click_context,\n            )\n\n        # Get all params associated with this key in both compile and sync\n        associated_params = (\n            cli_params[key]\n            for cli_params in (compile_cli_params, sync_cli_params)\n            if key in cli_params\n        )\n\n        # Validate value against types of all associated params\n        for param in associated_params:\n            try:\n                param.type_cast_value(value=value, ctx=click_context)\n            except Exception as e:\n                raise click.BadOptionUsage(\n                    option_name=key,\n                    message=(\n                        f\"Invalid value for config key {key!r}: {value!r}.{os.linesep}\"\n                        f\"Details: {e}\"\n                    ),\n                    ctx=click_context,\n                ) from e\n\n\ndef select_config_file(src_files: tuple[str, ...]) -> Path | None:\n    \"\"\"\n    Return the config file to use for defaults given ``src_files`` provided.\n    \"\"\"\n    # NOTE: If no src_files were specified, consider the current directory the\n    # NOTE: only config file lookup candidate. This usually happens when a\n    # NOTE: pip-tools invocation gets its incoming requirements from standard\n    # NOTE: input.\n    working_directory = Path.cwd()\n    src_files_as_paths = (\n        (working_directory / src_file).resolve() for src_file in src_files or (\".\",)\n    )\n    candidate_dirs = (src if src.is_dir() else src.parent for src in src_files_as_paths)\n    config_file_path = next(\n        (\n            candidate_dir / config_file\n            for candidate_dir in candidate_dirs\n            for config_file in DEFAULT_CONFIG_FILE_NAMES\n            if (candidate_dir / config_file).is_file()\n        ),\n        None,\n    )\n    if config_file_path is None:\n        return None\n\n    return (\n        config_file_path.relative_to(working_directory)\n        if config_file_path.is_relative_to(working_directory)\n        else config_file_path\n    )\n\n\ndef get_cli_options(ctx: click.Context) -> dict[str, click.Parameter]:\n    cli_opts = {\n        opt: option\n        for option in ctx.command.params\n        for opt in itertools.chain(option.opts, option.secondary_opts)\n        if opt.startswith(\"--\") and option.name is not None\n    }\n    return cli_opts\n\n\ndef parse_config_file(\n    click_context: click.Context, config_file: Path\n) -> dict[str, _t.Any]:\n    try:\n        config = _tomllib_compat.loads(config_file.read_text(encoding=\"utf-8\"))\n    except OSError as os_err:\n        raise click.FileError(\n            filename=str(config_file),\n            hint=f\"Could not read '{config_file !s}': {os_err !s}\",\n        )\n    except ValueError as value_err:\n        raise click.FileError(\n            filename=str(config_file),\n            hint=f\"Could not parse '{config_file !s}': {value_err !s}\",\n        )\n\n    # In a TOML file, we expect the config to be under `[tool.pip-tools]`,\n    # `[tool.pip-tools.compile]` or `[tool.pip-tools.sync]`\n    piptools_config: dict[str, _t.Any] = config.get(\"tool\", {}).get(\"pip-tools\", {})\n\n    assert click_context.command.name is not None\n    config_section_name = click_context.command.name.removeprefix(\"pip-\")\n\n    piptools_config.update(piptools_config.pop(config_section_name, {}))\n    piptools_config.pop(\"compile\", {})\n    piptools_config.pop(\"sync\", {})\n\n    piptools_config = _normalize_keys_in_config(piptools_config)\n    piptools_config = _invert_negative_bool_options_in_config(\n        ctx=click_context,\n        config=piptools_config,\n    )\n\n    return piptools_config\n\n\ndef _normalize_keys_in_config(config: dict[str, _t.Any]) -> dict[str, _t.Any]:\n    return {_normalize_config_key(key): value for key, value in config.items()}\n\n\ndef _invert_negative_bool_options_in_config(\n    ctx: click.Context, config: dict[str, _t.Any]\n) -> dict[str, _t.Any]:\n    new_config = {}\n    cli_opts = get_cli_options(ctx)\n\n    for key, value in config.items():\n        # Transform config key to its equivalent in the CLI\n        long_option = _convert_to_long_option(key)\n        new_key = cli_opts[long_option].name if long_option in cli_opts else key\n        negative_option_prefix = \"no_\"\n        assert new_key is not None\n        if (\n            new_key.startswith(negative_option_prefix)\n            and long_option not in ONLY_NEGATIVE_OPTIONS\n        ):\n            new_key = new_key[len(negative_option_prefix) :]\n\n        # Invert negative boolean according to the CLI\n        new_value = (\n            not value\n            if long_option.startswith(\"--no-\")\n            and long_option not in ONLY_NEGATIVE_OPTIONS\n            and isinstance(value, bool)\n            else value\n        )\n        new_config[new_key] = new_value\n\n    return new_config\n\n\ndef _normalize_config_key(key: str) -> str:\n    \"\"\"Transform given ``some-key`` into ``some_key``.\"\"\"\n    return key.lstrip(\"-\").replace(\"-\", \"_\").lower()\n\n\ndef _convert_to_long_option(key: str) -> str:\n    \"\"\"Transform given ``some-key`` into ``--some-key``.\"\"\"\n    return \"--\" + key.lstrip(\"-\").replace(\"_\", \"-\").lower()\n"
  },
  {
    "path": "piptools/writer.py",
    "content": "from __future__ import annotations\n\nimport contextlib\nimport io\nimport os\nimport re\nimport sys\nimport types\nimport typing as _t\nfrom collections.abc import Iterable, Iterator\nfrom itertools import chain\n\nfrom click import unstyle\nfrom click.core import Context\nfrom pip._internal.models.format_control import FormatControl\nfrom pip._internal.req.req_install import InstallRequirement\nfrom pip._vendor.packaging.markers import Marker\n\nfrom ._compat import canonicalize_name\nfrom .logging import log\nfrom .utils import (\n    comment,\n    dedup,\n    format_requirement,\n    get_compile_command,\n    key_from_ireq,\n    strip_extras,\n)\n\nif sys.version_info >= (3, 11):\n    from typing import Self as _t_Self\nelse:\n    from typing_extensions import Self as _t_Self\n\n\nMESSAGE_UNHASHED_PACKAGE = comment(\n    \"# WARNING: pip install will require the following package to be hashed.\"\n    \"\\n# Consider using a hashable URL like \"\n    \"https://github.com/jazzband/pip-tools/archive/SOMECOMMIT.zip\"\n)\n\nMESSAGE_UNSAFE_PACKAGES_UNPINNED = comment(\n    \"# WARNING: The following packages were not pinned, but pip requires them to be\"\n    \"\\n# pinned when the requirements file includes hashes and the requirement is not\"\n    \"\\n# satisfied by a package already installed. \"\n    \"Consider using the --allow-unsafe flag.\"\n)\n\nMESSAGE_UNSAFE_PACKAGES = comment(\n    \"# The following packages are considered to be unsafe in a requirements file:\"\n)\n\nMESSAGE_UNINSTALLABLE = (\n    \"The generated requirements file may be rejected by pip install. \"\n    \"See # WARNING lines for details.\"\n)\n\n\nstrip_comes_from_line_re = re.compile(r\" \\(line \\d+\\)$\")\n\n\ndef _comes_from_as_string(comes_from: str | InstallRequirement) -> str:\n    if isinstance(comes_from, str):\n        return strip_comes_from_line_re.sub(\"\", comes_from)\n    return canonicalize_name(key_from_ireq(comes_from))\n\n\ndef annotation_style_split(required_by: set[str]) -> str:\n    sorted_required_by = sorted(required_by)\n    if len(sorted_required_by) == 1:\n        source = sorted_required_by[0]\n        annotation = \"# via \" + source\n    else:\n        annotation_lines = [\"# via\"]\n        for source in sorted_required_by:\n            annotation_lines.append(\"    #   \" + source)\n        annotation = \"\\n\".join(annotation_lines)\n    return annotation\n\n\ndef annotation_style_line(required_by: set[str]) -> str:\n    return f\"# via {', '.join(sorted(required_by))}\"\n\n\nclass OutputWriter:\n    def __init__(\n        self,\n        dst_file: _t.BinaryIO,\n        click_ctx: Context,\n        dry_run: bool,\n        emit_header: bool,\n        emit_index_url: bool,\n        emit_trusted_host: bool,\n        annotate: bool,\n        annotation_style: str,\n        strip_extras: bool,\n        generate_hashes: bool,\n        default_index_url: str,\n        index_urls: Iterable[str],\n        trusted_hosts: Iterable[str],\n        format_control: FormatControl,\n        linesep: str,\n        allow_unsafe: bool,\n        find_links: list[str],\n        emit_find_links: bool,\n        emit_options: bool,\n    ) -> None:\n        self.dst_file = dst_file\n        self.click_ctx = click_ctx\n        self.dry_run = dry_run\n        self.emit_header = emit_header\n        self.emit_index_url = emit_index_url\n        self.emit_trusted_host = emit_trusted_host\n        self.annotate = annotate\n        self.annotation_style = annotation_style\n        self.strip_extras = strip_extras\n        self.generate_hashes = generate_hashes\n        self.default_index_url = default_index_url\n        self.index_urls = index_urls\n        self.trusted_hosts = trusted_hosts\n        self.format_control = format_control\n        self.linesep = linesep\n        self.allow_unsafe = allow_unsafe\n        self.find_links = find_links\n        self.emit_find_links = emit_find_links\n        self.emit_options = emit_options\n\n    def _sort_key(self, ireq: InstallRequirement) -> tuple[bool, str]:\n        return (not ireq.editable, key_from_ireq(ireq))\n\n    def write_header(self) -> Iterator[str]:\n        if self.emit_header:\n            yield comment(\"#\")\n            yield comment(\n                \"# This file is autogenerated by pip-compile with Python \"\n                f\"{sys.version_info.major}.{sys.version_info.minor}\"\n            )\n            yield comment(\"# by the following command:\")\n            yield comment(\"#\")\n            compile_command = os.environ.get(\n                \"CUSTOM_COMPILE_COMMAND\"\n            ) or get_compile_command(self.click_ctx)\n            yield comment(f\"#    {compile_command}\")\n            yield comment(\"#\")\n\n    def write_index_options(self) -> Iterator[str]:\n        if self.emit_index_url:\n            for index, index_url in enumerate(dedup(self.index_urls)):\n                if index == 0 and index_url.rstrip(\"/\") == self.default_index_url:\n                    continue\n                flag = \"--index-url\" if index == 0 else \"--extra-index-url\"\n                yield f\"{flag} {index_url}\"\n\n    def write_trusted_hosts(self) -> Iterator[str]:\n        if self.emit_trusted_host:\n            for trusted_host in dedup(self.trusted_hosts):\n                yield f\"--trusted-host {trusted_host}\"\n\n    def write_format_controls(self) -> Iterator[str]:\n        # The ordering of output needs to preserve the behavior of pip's\n        # FormatControl.get_allowed_formats(). The behavior is the following:\n        #\n        #   * Parsing of CLI options happens first to last.\n        #   * --only-binary takes precedence over --no-binary\n        #   * Package names take precedence over :all:\n        #   * We'll never see :all: in both due to mutual exclusion.\n        #\n        # So in summary, we want to emit :all: first and then package names later.\n        no_binary = self.format_control.no_binary.copy()\n        only_binary = self.format_control.only_binary.copy()\n\n        if \":all:\" in no_binary:\n            yield \"--no-binary :all:\"\n            no_binary.remove(\":all:\")\n        if \":all:\" in only_binary:\n            yield \"--only-binary :all:\"\n            only_binary.remove(\":all:\")\n        for nb in dedup(sorted(no_binary)):\n            yield f\"--no-binary {nb}\"\n        for ob in dedup(sorted(only_binary)):\n            yield f\"--only-binary {ob}\"\n\n    def write_find_links(self) -> Iterator[str]:\n        if self.emit_find_links:\n            for find_link in dedup(self.find_links):\n                yield f\"--find-links {find_link}\"\n\n    def write_flags(self) -> Iterator[str]:\n        if not self.emit_options:\n            return\n        emitted = False\n        for line in chain(\n            self.write_index_options(),\n            self.write_find_links(),\n            self.write_trusted_hosts(),\n            self.write_format_controls(),\n        ):\n            emitted = True\n            yield line\n        if emitted:\n            yield \"\"\n\n    def _iter_lines(\n        self,\n        results: set[InstallRequirement],\n        unsafe_requirements: set[InstallRequirement],\n        unsafe_packages: set[str],\n        markers: dict[str, Marker],\n        hashes: dict[InstallRequirement, set[str]] | None = None,\n    ) -> Iterator[str]:\n        # default values\n        unsafe_packages = unsafe_packages if self.allow_unsafe else set()\n        hashes = hashes or {}\n\n        # Check for unhashed or unpinned packages if at least one package does have\n        # hashes, which will trigger pip install's --require-hashes mode.\n        warn_uninstallable = False\n        has_hashes = hashes and any(hash for hash in hashes.values())\n\n        yielded = False\n\n        for line in self.write_header():\n            yield line\n            yielded = True\n        for line in self.write_flags():\n            yield line\n            yielded = True\n\n        unsafe_requirements = unsafe_requirements or {\n            r for r in results if r.name in unsafe_packages\n        }\n        packages = {r for r in results if r.name not in unsafe_packages}\n\n        if packages:\n            for ireq in sorted(packages, key=self._sort_key):\n                if has_hashes and not hashes.get(ireq):\n                    yield MESSAGE_UNHASHED_PACKAGE\n                    warn_uninstallable = True\n                line = self._format_requirement(\n                    ireq, markers.get(key_from_ireq(ireq)), hashes=hashes\n                )\n                yield line\n            yielded = True\n\n        if unsafe_requirements:\n            yield \"\"\n            yielded = True\n            if has_hashes and not self.allow_unsafe:\n                yield MESSAGE_UNSAFE_PACKAGES_UNPINNED\n                warn_uninstallable = True\n            else:\n                yield MESSAGE_UNSAFE_PACKAGES\n\n            for ireq in sorted(unsafe_requirements, key=self._sort_key):\n                ireq_key = key_from_ireq(ireq)\n                if not self.allow_unsafe:\n                    yield comment(f\"# {ireq_key}\")\n                else:\n                    line = self._format_requirement(\n                        ireq, marker=markers.get(ireq_key), hashes=hashes\n                    )\n                    yield line\n\n        # Yield even when there's no real content, so that blank files are written\n        if not yielded:\n            yield \"\"\n\n        if warn_uninstallable:\n            log.warning(MESSAGE_UNINSTALLABLE)\n\n    def write(\n        self,\n        results: set[InstallRequirement],\n        unsafe_requirements: set[InstallRequirement],\n        unsafe_packages: set[str],\n        markers: dict[str, Marker],\n        hashes: dict[InstallRequirement, set[str]] | None,\n    ) -> None:\n        attached_writer: contextlib.AbstractContextManager[_LineWriter]\n        if not self.dry_run:\n            attached_writer = _FileLineWriter(self.dst_file, self.linesep)\n        else:\n            attached_writer = contextlib.nullcontext(_dry_run_line_writer)\n\n        with attached_writer as write_line:\n            for line in self._iter_lines(\n                results, unsafe_requirements, unsafe_packages, markers, hashes\n            ):\n                write_line(line)\n\n    def _format_requirement(\n        self,\n        ireq: InstallRequirement,\n        marker: Marker | None = None,\n        hashes: dict[InstallRequirement, set[str]] | None = None,\n    ) -> str:\n        ireq_hashes = (hashes if hashes is not None else {}).get(ireq)\n\n        line = format_requirement(ireq, marker=marker, hashes=ireq_hashes)\n        if self.strip_extras:\n            line = strip_extras(line)\n\n        if not self.annotate:\n            return line\n\n        # Annotate what packages or reqs-ins this package is required by\n        required_by = set()\n        if hasattr(ireq, \"_source_ireqs\"):\n            required_by |= {\n                _comes_from_as_string(src_ireq.comes_from)\n                for src_ireq in ireq._source_ireqs\n                if src_ireq.comes_from\n            }\n\n        # Filter out the origin install requirements for extras.\n        # See https://github.com/jazzband/pip-tools/issues/2003\n        if ireq.comes_from and (\n            isinstance(ireq.comes_from, str) or ireq.comes_from.name != ireq.name\n        ):\n            required_by.add(_comes_from_as_string(ireq.comes_from))\n\n        required_by |= set(getattr(ireq, \"_required_by\", set()))\n\n        if required_by:\n            if self.annotation_style == \"split\":\n                annotation = annotation_style_split(required_by)\n                sep = \"\\n    \"\n            elif self.annotation_style == \"line\":\n                annotation = annotation_style_line(required_by)\n                sep = \"\\n    \" if ireq_hashes else \"  \"\n            else:  # pragma: no cover\n                raise ValueError(\"Invalid value for annotation style\")\n            if self.strip_extras:\n                annotation = strip_extras(annotation)\n            # 24 is one reasonable column size to use here, that we've used in the past\n            lines = f\"{line:24}{sep}{comment(annotation)}\".splitlines()\n            line = \"\\n\".join(ln.rstrip() for ln in lines)\n\n        return line\n\n\nclass _LineWriter(_t.Protocol):\n    def __call__(self, line: str) -> None: ...\n\n\ndef _dry_run_line_writer(line: str) -> None:\n    log.log(line)\n\n\nclass _FileLineWriter:\n    def __init__(self, dst_file: _t.BinaryIO, linesep: str) -> None:\n        self.dst_file = io.TextIOWrapper(\n            dst_file,\n            encoding=\"utf8\",\n            newline=linesep,\n            line_buffering=True,\n        )\n\n    def __call__(self, line: str) -> None:\n        log.info(line)\n        self.dst_file.write(unstyle(line))\n        self.dst_file.write(\"\\n\")\n\n    def __enter__(self) -> _t_Self:\n        return self\n\n    def __exit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_val: BaseException | None,\n        exc_tb: types.TracebackType | None,\n    ) -> None:\n        self.dst_file.detach()\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools>=63\", \"setuptools_scm[toml]>=7\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\n# https://peps.python.org/pep-0621/#readme\nrequires-python = \">= 3.9\"\ndynamic = [\"version\"]\nname = \"pip-tools\"\ndescription = \"pip-tools keeps your pinned dependencies fresh.\"\nreadme = \"README.md\"\nauthors = [{ \"name\" = \"Vincent Driessen\", \"email\" = \"me@nvie.com\" }]\nlicense = { text = \"BSD\" }\nclassifiers = [\n  \"Development Status :: 5 - Production/Stable\",\n  \"Environment :: Console\",\n  \"Intended Audience :: Developers\",\n  \"Intended Audience :: System Administrators\",\n  \"License :: OSI Approved :: BSD License\",\n  \"Operating System :: OS Independent\",\n  \"Programming Language :: Python :: 3 :: Only\",\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  \"Programming Language :: Python :: 3.14\",\n  \"Programming Language :: Python :: Implementation :: CPython\",\n  \"Programming Language :: Python :: Implementation :: PyPy\",\n  \"Programming Language :: Python\",\n  \"Topic :: Software Development :: Quality Assurance\",\n  \"Topic :: Software Development :: Testing\",\n  \"Topic :: System :: Systems Administration\",\n  \"Topic :: Utilities\",\n  \"Typing :: Typed\",\n]\nkeywords = [\"pip\", \"requirements\", \"packaging\"]\ndependencies = [\n  # direct dependencies\n  \"build >= 1.0.0\",\n  \"click >= 8\",\n  \"pip >= 22.2\",\n  \"pyproject_hooks\",\n  \"tomli; python_version < '3.11'\",\n  # indirect dependencies\n  \"setuptools\", # typically needed when pip-tools invokes setup.py\n  \"wheel\", # pip plugin needed by pip-tools\n\n]\n\n[project.urls]\nhomepage = \"https://github.com/jazzband/pip-tools/\"\ndocumentation = \"https://pip-tools.readthedocs.io/en/latest/\"\nrepository = \"https://github.com/jazzband/pip-tools\"\nchangelog = \"https://github.com/jazzband/pip-tools/releases\"\n\n[project.optional-dependencies]\ntesting = [\n  \"pytest >= 7.2.0\",\n  \"pytest-rerunfailures\",\n  \"pytest-xdist\",\n  \"tomli-w\",\n  # build deps for tests\n  \"flit_core >=2,<4\",\n  \"poetry_core>=1.0.0\",\n]\ncoverage = [\"covdefaults\", \"pytest-cov\"]\n\n[project.scripts]\npip-compile = \"piptools.scripts.compile:cli\"\npip-sync = \"piptools.scripts.sync:cli\"\n\n[tool.isort]\nprofile = \"black\"\n# explicitly mark 'build' as a third-party package\n# otherwise, in some executions, `isort` can mistake `piptools.build` for\n# `build` and treat it as a first-party module name\nknown_third_party = [\"build\"]\nadd_imports = \"from __future__ import annotations\"\n\n[tool.mypy]\ndisallow_untyped_defs = true\ndisallow_any_generics = true\ndisallow_incomplete_defs = true\ndisallow_subclassing_any = true\ndisallow_untyped_calls = true\ndisallow_untyped_decorators = true\nignore_missing_imports = true\nno_implicit_optional = true\nno_implicit_reexport = true\nstrict_equality = true\nwarn_redundant_casts = true\nwarn_return_any = true\nwarn_unused_configs = true\nwarn_unused_ignores = true\n# Avoid error: Duplicate module named 'setup'\n# https://github.com/python/mypy/issues/4008\nexclude = \"^tests/test_data/\"\n\n[[tool.mypy.overrides]]\nmodule = [\"tests.*\"]\ndisallow_untyped_defs = false\ndisallow_incomplete_defs = false\n\n[tool.pytest.ini_options]\naddopts = [\n  # `pytest-xdist`:\n  \"--numprocesses=auto\",\n\n  # The `worksteal` distribution method is useful if the run times of different tests vary greatly,\n  # as it ensures more efficient resource usage, improving the performance of testing.\n  \"--dist=worksteal\",\n\n  # Show 20 slowest invocations:\n  \"--durations=20\",\n]\nnorecursedirs = \".* build dist venv test_data piptools/_compat/*\"\ntestpaths = \"tests piptools\"\nfilterwarnings = [\"always\"]\nmarkers = [\"network: mark tests that require internet access\"]\n\n[tool.setuptools.packages.find]\n# needed only because we did not adopt src layout yet\ninclude = [\"piptools*\"]\n\n[tool.setuptools_scm]\nlocal_scheme = \"dirty-tag\"\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/conftest.py",
    "content": "from __future__ import annotations\n\nimport json\nimport os\nimport platform\nimport shutil\nimport subprocess\nimport sys\nimport typing as _t\nfrom contextlib import contextmanager\nfrom dataclasses import dataclass, field\nfrom functools import partial\nfrom importlib.metadata import version as version_of\nfrom pathlib import Path\nfrom textwrap import dedent\n\nimport pytest\nimport tomli_w\nfrom click.testing import CliRunner\nfrom pip._internal.commands.install import InstallCommand\nfrom pip._internal.index.package_finder import PackageFinder\nfrom pip._internal.models.candidate import InstallationCandidate\nfrom pip._internal.models.link import Link\nfrom pip._internal.network.session import PipSession\nfrom pip._internal.req.constructors import (\n    install_req_from_editable,\n    install_req_from_line,\n)\nfrom pip._internal.utils.direct_url_helpers import direct_url_from_link\nfrom pip._vendor.packaging.version import Version\nfrom pip._vendor.pkg_resources import Requirement\n\nfrom piptools._compat import Distribution\nfrom piptools._internal import _pip_api\nfrom piptools.cache import DependencyCache\nfrom piptools.exceptions import NoCandidateFound\nfrom piptools.locations import DEFAULT_CONFIG_FILE_NAMES\nfrom piptools.logging import log\nfrom piptools.repositories import PyPIRepository\nfrom piptools.repositories.base import BaseRepository\nfrom piptools.resolver import BacktrackingResolver, LegacyResolver\nfrom piptools.utils import (\n    as_tuple,\n    is_url_requirement,\n    key_from_ireq,\n)\n\nfrom .constants import MINIMAL_WHEELS_PATH, TEST_DATA_PATH\nfrom .utils import looks_like_ci\n\n\n@dataclass\nclass FakeOptions:\n    features_enabled: list[str] = field(default_factory=list)\n    deprecated_features_enabled: list[str] = field(default_factory=list)\n    target_dir: str | None = None\n\n\nclass FakeRepository(BaseRepository):\n    def __init__(self, options: FakeOptions):\n        self._options = options\n\n        with open(os.path.join(TEST_DATA_PATH, \"fake-index.json\")) as f:\n            self.index = json.load(f)\n\n        with open(os.path.join(TEST_DATA_PATH, \"fake-editables.json\")) as f:\n            self.editables = json.load(f)\n\n    def get_hashes(self, ireq):\n        # Some fake hashes\n        return {\n            \"test:123\",\n            \"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\n        }\n\n    def find_best_match(self, ireq, prereleases=False):\n        if ireq.editable:\n            return ireq\n\n        versions = list(\n            ireq.specifier.filter(\n                self.index[key_from_ireq(ireq)], prereleases=prereleases\n            )\n        )\n        if not versions:\n            tried_versions = [\n                InstallationCandidate(ireq.name, version, \"https://fake.url.foo\")\n                for version in self.index[key_from_ireq(ireq)]\n            ]\n            raise NoCandidateFound(ireq, tried_versions, [\"https://fake.url.foo\"])\n        best_version = max(versions, key=Version)\n        return _pip_api.create_install_requirement(\n            key_from_ireq(ireq), best_version, ireq\n        )\n\n    def get_dependencies(self, ireq):\n        if ireq.editable or is_url_requirement(ireq):\n            return self.editables[str(ireq.link)]\n\n        name, version, extras = as_tuple(ireq)\n        # Store non-extra dependencies under the empty string\n        extras += (\"\",)\n        dependencies = [\n            dep for extra in extras for dep in self.index[name][version][extra]\n        ]\n        return [\n            install_req_from_line(dep, constraint=ireq.constraint)\n            for dep in dependencies\n        ]\n\n    @contextmanager\n    def allow_all_wheels(self):\n        # No need to do an actual pip.Wheel mock here.\n        yield\n\n    @property\n    def options(self):\n        return self._options\n\n    @property\n    def session(self) -> PipSession:\n        \"\"\"Not used\"\"\"\n\n    @property\n    def finder(self) -> PackageFinder:\n        \"\"\"Not used\"\"\"\n\n    @property\n    def command(self) -> InstallCommand:\n        \"\"\"Not used\"\"\"\n\n\ndef pytest_collection_modifyitems(config, items):\n    for item in items:\n        # Mark network tests as flaky\n        if item.get_closest_marker(\"network\") and looks_like_ci():\n            item.add_marker(pytest.mark.flaky(reruns=3, reruns_delay=2))\n\n\n@pytest.fixture\ndef fake_dist():\n    def _fake_dist(line, deps=None):\n        if deps is None:\n            deps = []\n        req = Requirement.parse(line)\n        key = req.name\n        if \"==\" in line:\n            version = line.split(\"==\")[1]\n        else:\n            version = \"0+unknown\"\n        requires = [Requirement.parse(d) for d in deps]\n        direct_url = None\n        if req.url:\n            direct_url = direct_url_from_link(Link(req.url))\n        return Distribution(key, version, requires, direct_url)\n\n    return _fake_dist\n\n\n@pytest.fixture\ndef repository():\n    return FakeRepository(\n        options=FakeOptions(deprecated_features_enabled=[\"legacy-resolver\"])\n    )\n\n\n@pytest.fixture\ndef pypi_repository(tmpdir):\n    return PyPIRepository(\n        [\n            \"--index-url\",\n            PyPIRepository.DEFAULT_INDEX_URL,\n            \"--use-deprecated\",\n            \"legacy-resolver\",\n        ],\n        cache_dir=(tmpdir / \"pypi-repo\"),\n    )\n\n\n@pytest.fixture\ndef depcache(tmpdir):\n    return DependencyCache(tmpdir / \"dep-cache\")\n\n\n@pytest.fixture\ndef resolver(depcache, repository):\n    # TODO: It'd be nicer if Resolver instance could be set up and then\n    #       use .resolve(...) on the specset, instead of passing it to\n    #       the constructor like this (it's not reusable)\n    return partial(\n        LegacyResolver, repository=repository, cache=depcache, existing_constraints={}\n    )\n\n\n@pytest.fixture\ndef backtracking_resolver(depcache):\n    # TODO: It'd be nicer if Resolver instance could be set up and then\n    #       use .resolve(...) on the specset, instead of passing it to\n    #       the constructor like this (it's not reusable)\n    return partial(\n        BacktrackingResolver,\n        repository=FakeRepository(options=FakeOptions()),\n        cache=depcache,\n        existing_constraints={},\n    )\n\n\n@pytest.fixture\ndef base_resolver(depcache):\n    return partial(LegacyResolver, cache=depcache, existing_constraints={})\n\n\n@pytest.fixture\ndef from_line():\n    def _from_line(*args, **kwargs):\n        if _pip_api.PIP_VERSION_MAJOR_MINOR <= (23, 0):\n            hash_options = kwargs.pop(\"hash_options\", {})\n            options = kwargs.pop(\"options\", {})\n            options[\"hashes\"] = hash_options\n            kwargs[\"options\"] = options\n        return install_req_from_line(*args, **kwargs)\n\n    return _from_line\n\n\n@pytest.fixture\ndef from_editable():\n    return install_req_from_editable\n\n\n@pytest.fixture\ndef runner():\n    if Version(version_of(\"click\")) < Version(\"8.2\"):\n        cli_runner = CliRunner(mix_stderr=False)\n    else:\n        cli_runner = CliRunner()\n    with cli_runner.isolated_filesystem():\n        yield cli_runner\n\n\n@pytest.fixture\ndef tmpdir_cwd(tmpdir):\n    with tmpdir.as_cwd():\n        yield Path(tmpdir)\n\n\n@pytest.fixture\ndef make_pip_conf(tmpdir, monkeypatch):\n    created_paths = []\n\n    def _make_pip_conf(content):\n        pip_conf_file = \"pip.conf\" if os.name != \"nt\" else \"pip.ini\"\n        path = (tmpdir / pip_conf_file).strpath\n\n        with open(path, \"w\") as f:\n            f.write(content)\n\n        monkeypatch.setenv(\"PIP_CONFIG_FILE\", path)\n\n        created_paths.append(path)\n        return path\n\n    try:\n        yield _make_pip_conf\n    finally:\n        for path in created_paths:\n            os.remove(path)\n\n\n@pytest.fixture\ndef pip_conf(make_pip_conf, minimal_wheels_path):\n    return make_pip_conf(dedent(f\"\"\"\\\n            [global]\n            no-index = true\n            find-links = {minimal_wheels_path.as_posix()}\n            \"\"\"))\n\n\n@pytest.fixture\ndef pip_with_index_conf(make_pip_conf, minimal_wheels_path):\n    return make_pip_conf(dedent(f\"\"\"\\\n            [global]\n            index-url = http://example.com\n            find-links = {minimal_wheels_path.as_posix()}\n            \"\"\"))\n\n\n@pytest.fixture(scope=\"session\")\ndef make_package(tmp_path_factory):\n    \"\"\"\n    Make a package from a given name, version and list of required packages.\n    \"\"\"\n\n    def _make_package(\n        name,\n        version=\"0.1\",\n        install_requires=None,\n        extras_require=None,\n        build_system_requires=None,\n    ):\n        if install_requires is None:\n            install_requires = []\n\n        if extras_require is None:\n            extras_require = dict()\n\n        install_requires_str = \"[{}]\".format(\n            \",\".join(f\"{package!r}\" for package in install_requires)\n        )\n\n        package_dir = tmp_path_factory.mktemp(\"packages\") / name / version\n        package_dir.mkdir(parents=True)\n\n        with (package_dir / \"setup.py\").open(\"w\") as fp:\n            fp.write(dedent(f\"\"\"\\\n                    from setuptools import setup\n                    setup(\n                        name={name!r},\n                        version={version!r},\n                        author=\"pip-tools\",\n                        author_email=\"pip-tools@localhost\",\n                        url=\"https://github.com/jazzband/pip-tools\",\n                        install_requires={install_requires_str},\n                        extras_require={extras_require},\n                        py_modules=[{name!r}],\n                    )\n                    \"\"\"))\n\n        # Create a README to avoid setuptools warnings.\n        (package_dir / \"README\").touch()\n\n        # Create a module to make the package importable.\n        (package_dir / name).with_suffix(\".py\").touch()\n\n        if build_system_requires:\n            with (package_dir / \"pyproject.toml\").open(\"w\") as fp:\n                fp.write(dedent(f\"\"\"\\\n                        [build-system]\n                        requires = {json.dumps(build_system_requires)}\n                        \"\"\"))\n\n        return package_dir\n\n    return _make_package\n\n\n@pytest.fixture(scope=\"session\")\ndef run_setup_file():\n    \"\"\"\n    Run a setup.py file from a given package dir.\n    \"\"\"\n\n    def _run_setup_file(package_dir_path, *args):\n        setup_file = package_dir_path / \"setup.py\"\n        return subprocess.run(\n            [sys.executable, str(setup_file), *args],\n            cwd=str(package_dir_path),\n            stdout=subprocess.DEVNULL,\n            check=True,\n        )  # nosec\n\n    return _run_setup_file\n\n\n@pytest.fixture(scope=\"session\")\ndef make_wheel(run_setup_file):\n    \"\"\"\n    Make a wheel distribution from a given package dir.\n    \"\"\"\n\n    def _make_wheel(package_dir, dist_dir, *args):\n        return run_setup_file(\n            package_dir, \"bdist_wheel\", \"--dist-dir\", str(dist_dir), *args\n        )\n\n    return _make_wheel\n\n\n@pytest.fixture\ndef make_sdist(run_setup_file):\n    \"\"\"\n    Make a source distribution from a given package dir.\n    \"\"\"\n\n    def _make_sdist(package_dir, dist_dir, *args):\n        return run_setup_file(package_dir, \"sdist\", \"--dist-dir\", str(dist_dir), *args)\n\n    return _make_sdist\n\n\n@pytest.fixture\ndef make_module(tmpdir):\n    \"\"\"\n    Make a metadata file with the given name and content and a fake module.\n    \"\"\"\n\n    def _make_module(fname, content):\n        path = os.path.join(tmpdir, \"sample_lib\")\n        os.mkdir(path)\n        path = os.path.join(tmpdir, \"sample_lib\", \"__init__.py\")\n        with open(path, \"w\") as stream:\n            stream.write(\"'example module'\\n__version__ = '1.2.3'\")\n        if fname == \"setup.cfg\":\n            path = os.path.join(tmpdir, \"pyproject.toml\")\n            with open(path, \"w\") as stream:\n                stream.write(\n                    \"\\n\".join(\n                        (\n                            \"[build-system]\",\n                            'requires = [\"setuptools\"]',\n                            'build-backend = \"setuptools.build_meta\"',\n                        )\n                    )\n                )\n        path = os.path.join(tmpdir, fname)\n        with open(path, \"w\") as stream:\n            stream.write(dedent(content))\n        return path\n\n    return _make_module\n\n\n@pytest.fixture(scope=\"session\")\ndef fake_dists(tmp_path_factory, make_package, make_wheel):\n    \"\"\"\n    Generate distribution packages `small-fake-*`\n    \"\"\"\n    dists_path = tmp_path_factory.mktemp(\"dists\")\n    pkgs = [\n        make_package(\"small-fake-a\", version=\"0.1\"),\n        make_package(\"small-fake-b\", version=\"0.2\"),\n        make_package(\"small-fake-c\", version=\"0.3\"),\n    ]\n    for pkg in pkgs:\n        make_wheel(pkg, dists_path)\n    return dists_path\n\n\n@pytest.fixture(scope=\"session\")\ndef fake_dists_with_build_deps(tmp_path_factory, make_package, make_wheel):\n    \"\"\"Generate distribution packages with names that make sense for testing build deps.\"\"\"\n    dists_path = tmp_path_factory.mktemp(\"dists\")\n    pkgs = [\n        make_package(\n            \"fake_static_build_dep\",\n            version=\"0.1\",\n            install_requires=[\"fake_transient_run_dep\"],\n            build_system_requires=[\"fake_transient_build_dep\"],\n        ),\n        make_package(\"fake_dynamic_build_dep_for_all\", version=\"0.2\"),\n        make_package(\"fake_dynamic_build_dep_for_sdist\", version=\"0.3\"),\n        make_package(\"fake_dynamic_build_dep_for_wheel\", version=\"0.4\"),\n        make_package(\"fake_dynamic_build_dep_for_editable\", version=\"0.5\"),\n        make_package(\"fake_direct_runtime_dep\", version=\"0.1\"),\n        make_package(\"fake_direct_extra_runtime_dep\", version=\"0.2\"),\n        make_package(\"fake_transient_build_dep\", version=\"0.3\"),\n        make_package(\"fake_transient_run_dep\", version=\"0.3\"),\n    ]\n    for pkg in pkgs:\n        make_wheel(pkg, dists_path)\n    return dists_path\n\n\n@pytest.fixture\ndef venv(tmp_path):\n    \"\"\"Create a temporary venv and get the path of its directory of executables.\"\"\"\n    subprocess.run(\n        [sys.executable, \"-m\", \"venv\", os.fspath(tmp_path)],\n        check=True,\n    )\n    return tmp_path / (\"Scripts\" if platform.system() == \"Windows\" else \"bin\")\n\n\n@pytest.fixture(autouse=True)\ndef _reset_log():\n    \"\"\"\n    Since piptools.logging.log is a global variable we have to restore its initial\n    state. Some tests can change logger verbosity which might cause a conflict\n    with other tests that depend on it.\n    \"\"\"\n    log.reset()\n\n\n@pytest.fixture\ndef make_config_file(tmpdir_cwd):\n    \"\"\"\n    Make a config file for pip-tools with a given parameter set to a specific\n    value, returning a ``pathlib.Path`` to the config file.\n    \"\"\"\n\n    def _maker(\n        pyproject_param: str,\n        new_default: _t.Any,\n        config_file_name: str = DEFAULT_CONFIG_FILE_NAMES[0],\n        section: str = \"pip-tools\",\n        subsection: str | None = None,\n    ) -> Path:\n        # Create a nested directory structure if config_file_name includes directories\n        config_dir = (tmpdir_cwd / config_file_name).parent\n        config_dir.mkdir(exist_ok=True, parents=True)\n\n        # Make a config file with this one config default override\n        config_file = tmpdir_cwd / config_file_name\n\n        nested_config = {pyproject_param: new_default}\n        if subsection:\n            nested_config = {subsection: nested_config}\n        config_to_dump = {\"tool\": {section: nested_config}}\n        config_file.write_text(tomli_w.dumps(config_to_dump))\n        return _t.cast(Path, config_file.relative_to(tmpdir_cwd))\n\n    return _maker\n\n\n@pytest.fixture(scope=\"session\")\ndef setuptools_wheel_path(tmp_path_factory):\n    \"\"\"\n    Ensure setuptools is downloaded and return the path to the download directory.\n    \"\"\"\n    tmp_wheels_path = tmp_path_factory.mktemp(\"tmp_wheels\")\n    subprocess.check_call(\n        [\n            sys.executable,\n            \"-Im\",\n            \"pip\",\n            \"download\",\n            f\"--dest={tmp_wheels_path.as_posix()}\",\n            \"--no-deps\",\n            \"setuptools\",\n        ],\n    )\n    return tmp_wheels_path\n\n\n@pytest.fixture(scope=\"session\")\ndef minimal_wheels_path(setuptools_wheel_path):\n    \"\"\"\n    Ensure minimal wheels and setuptools are downloaded and return the path.\n    \"\"\"\n    shutil.copytree(MINIMAL_WHEELS_PATH, setuptools_wheel_path, dirs_exist_ok=True)\n    return setuptools_wheel_path\n"
  },
  {
    "path": "tests/constants.py",
    "content": "from __future__ import annotations\n\nimport os\n\nTEST_DATA_PATH = os.path.join(os.path.dirname(__file__), \"test_data\")\nMINIMAL_WHEELS_PATH = os.path.join(TEST_DATA_PATH, \"minimal_wheels\")\nPACKAGES_PATH = os.path.join(TEST_DATA_PATH, \"packages\")\nPACKAGES_RELATIVE_PATH = os.path.relpath(\n    PACKAGES_PATH, os.path.commonpath([os.getcwd(), PACKAGES_PATH])\n)\n"
  },
  {
    "path": "tests/test_build.py",
    "content": "from __future__ import annotations\n\nimport pathlib\nimport shutil\nimport textwrap\n\nimport pytest\nfrom build import BuildBackendException\n\nfrom piptools.build import (\n    ProjectMetadata,\n    StaticProjectMetadata,\n    build_project_metadata,\n    maybe_statically_parse_project_metadata,\n)\nfrom tests.constants import PACKAGES_PATH\n\n\n@pytest.mark.network\ndef test_build_project_metadata_resolved_correct_build_dependencies(\n    fake_dists_with_build_deps, tmp_path, monkeypatch\n):\n    \"\"\"Test that the resolved build dependencies are correct.\n\n    Because this is a slow process we test it only for one build target and rely\n    on ``test_all_extras_and_all_build_deps`` to test that it works with multiple build\n    targets.\n    \"\"\"\n    # When used as argument to the runner it is not passed to pip\n    monkeypatch.setenv(\"PIP_FIND_LINKS\", fake_dists_with_build_deps)\n    src_pkg_path = pathlib.Path(PACKAGES_PATH) / \"small_fake_with_build_deps\"\n    shutil.copytree(src_pkg_path, tmp_path, dirs_exist_ok=True)\n    src_file = tmp_path / \"setup.py\"\n    metadata = build_project_metadata(\n        src_file, (\"editable\",), attempt_static_parse=False, isolated=True, quiet=False\n    )\n    assert isinstance(metadata, ProjectMetadata)\n    build_requirements = sorted(r.name for r in metadata.build_requirements)\n    assert build_requirements == [\n        \"fake_dynamic_build_dep_for_all\",\n        \"fake_dynamic_build_dep_for_editable\",\n        \"fake_static_build_dep\",\n        \"setuptools\",\n        \"wheel\",\n    ]\n\n\ndef test_build_project_metadata_static(tmp_path):\n    \"\"\"Test static parsing branch of build_project_metadata\"\"\"\n    src_pkg_path = pathlib.Path(PACKAGES_PATH) / \"small_fake_with_pyproject\"\n    shutil.copytree(src_pkg_path, tmp_path, dirs_exist_ok=True)\n    src_file = tmp_path / \"pyproject.toml\"\n    metadata = build_project_metadata(\n        src_file, (), attempt_static_parse=True, isolated=True, quiet=False\n    )\n    assert isinstance(metadata, StaticProjectMetadata)\n    requirements = [(r.name, r.extras, str(r.markers)) for r in metadata.requirements]\n    requirements.sort(key=lambda x: x[0])\n    assert requirements == [\n        (\"fake_direct_extra_runtime_dep\", {\"with_its_own_extra\"}, 'extra == \"x\"'),\n        (\"fake_direct_runtime_dep\", set(), \"None\"),\n    ]\n    assert metadata.extras == (\"x\",)\n\n\ndef test_build_project_metadata_raises_error(tmp_path):\n    src_pkg_path = pathlib.Path(PACKAGES_PATH) / \"small_fake_with_build_deps\"\n    shutil.copytree(src_pkg_path, tmp_path, dirs_exist_ok=True)\n    src_file = tmp_path / \"setup.py\"\n    with pytest.raises(\n        ValueError, match=\"Cannot execute the PEP 517 optional.* hooks statically\"\n    ):\n        build_project_metadata(\n            src_file,\n            (\"editable\",),\n            attempt_static_parse=True,\n            isolated=True,\n            quiet=False,\n        )\n\n\ndef test_build_project_metadata_upgrading_raises_error(tmp_path):\n    \"\"\"Test build_project_metadata doesn't swallow error.\"\"\"\n    src_file = tmp_path / \"pyproject.toml\"\n    src_file.write_text(\n        textwrap.dedent(\"\"\"\n            [project]\n            # missing name\n            version = \"0.1\"\n            dependencies=[\"test_dep\"]\n            \"\"\"),\n    )\n    with pytest.raises(\n        BuildBackendException,\n        match=(\n            \"Backend subprocess exited when trying to invoke \"\n            \"get_requires_for_build_wheel\"\n        ),\n    ):\n        build_project_metadata(\n            src_file,\n            (),\n            attempt_static_parse=False,\n            isolated=True,\n            quiet=False,\n            upgrade_packages=[\"test_dep\"],\n        )\n\n\ndef test_static_parse_valid(tmp_path):\n    src_file = tmp_path / \"pyproject.toml\"\n\n    valid = \"\"\"\n[project]\nname = \"foo\"\nversion = \"0.1.0\"\ndependencies = [\"bar>=1\"]\n[project.optional-dependencies]\nbaz = [\"qux[extra]\"]\n\"\"\"\n    src_file.write_text(valid)\n    metadata = maybe_statically_parse_project_metadata(src_file)\n    assert isinstance(metadata, StaticProjectMetadata)\n    assert [str(r.req) for r in metadata.requirements] == [\"bar>=1\", \"qux[extra]\"]\n    assert metadata.extras == (\"baz\",)\n\n\n@pytest.mark.parametrize(\n    \"input_path_is_absolute\",\n    (True, False),\n    ids=(\"absolute-input\", \"relative-input\"),\n)\ndef test_static_parse_of_self_referential_extra(\n    tmp_path, monkeypatch, input_path_is_absolute\n):\n    monkeypatch.chdir(tmp_path)\n\n    src_file = tmp_path / \"pyproject.toml\"\n    src_file.write_text(textwrap.dedent(\"\"\"\n            [project]\n            name = \"foo\"\n            version = \"0.1.0\"\n            [project.optional-dependencies]\n            ext1 = [\"bar\"]\n            ext2 = [\"foo[ext1]\"]\n            \"\"\"))\n\n    if input_path_is_absolute:\n        parse_path = src_file\n    else:\n        parse_path = src_file.relative_to(tmp_path)\n\n    metadata = maybe_statically_parse_project_metadata(parse_path)\n    assert isinstance(metadata, StaticProjectMetadata)\n    assert metadata.extras == (\"ext1\", \"ext2\")\n    assert len(metadata.requirements) == 2\n\n    assert [r.name for r in metadata.requirements] == [\"bar\", \"foo\"]\n    assert [r.comes_from for r in metadata.requirements] == [\n        f\"foo ({parse_path.as_posix()})\"\n    ] * 2\n\n    foo_req = metadata.requirements[1]\n    assert foo_req.extras == {\"ext1\"}\n    assert foo_req.link.url == tmp_path.as_uri()\n\n\ndef test_static_parse_invalid(tmp_path):\n    src_file = tmp_path / \"pyproject.toml\"\n\n    invalid_toml = \"\"\"this is not valid toml\"\"\"\n    src_file.write_text(invalid_toml)\n    assert maybe_statically_parse_project_metadata(src_file) is None\n\n    no_pep621 = \"\"\"\n[build-system]\nrequires = [\"setuptools\"]\n\"\"\"\n    src_file.write_text(no_pep621)\n    assert maybe_statically_parse_project_metadata(src_file) is None\n\n    invalid_pep621 = \"\"\"\n[project]\n# no name\nversion = \"0.1.0\"\n\"\"\"\n    src_file.write_text(invalid_pep621)\n    assert maybe_statically_parse_project_metadata(src_file) is None\n\n    dynamic_deps = \"\"\"\n[project]\nname = \"foo\"\ndynamic = [\"dependencies\"]\n\"\"\"\n    src_file.write_text(dynamic_deps)\n    assert maybe_statically_parse_project_metadata(src_file) is None\n\n    dynamic_optional_deps = \"\"\"\n[project]\nname = \"foo\"\ndynamic = [\"optional-dependencies\"]\n\"\"\"\n    src_file.write_text(dynamic_optional_deps)\n    assert maybe_statically_parse_project_metadata(src_file) is None\n\n    src_file = tmp_path / \"setup.py\"\n    src_file.write_text(\"print('hello')\")\n    assert maybe_statically_parse_project_metadata(src_file) is None\n\n\n@pytest.mark.network\ndef test_build_metadata_from_dynamic_dependencies(tmp_path):\n    pyproject_file = tmp_path / \"pyproject.toml\"\n    setuppy_file = tmp_path / \"setup.py\"\n\n    pyproject_file.write_text(textwrap.dedent(\"\"\"\n            [project]\n            name = \"foo\"\n            version = \"0.1.0\"\n            dynamic = [\"dependencies\"]\n            \"\"\"))\n    setuppy_file.write_text(textwrap.dedent(\"\"\"\\\n            from setuptools import setup\n            setup(install_requires=[\"bar > 2\"])\n            \"\"\"))\n\n    metadata = build_project_metadata(\n        pyproject_file, (), attempt_static_parse=True, isolated=True, quiet=False\n    )\n    assert isinstance(metadata, ProjectMetadata)\n    assert [str(r.req) for r in metadata.requirements] == [\"bar>2\"]\n    assert [r.comes_from for r in metadata.requirements] == [\n        f\"foo ({pyproject_file.as_posix()})\"\n    ]\n"
  },
  {
    "path": "tests/test_cache.py",
    "content": "from __future__ import annotations\n\nimport os\nimport sys\nfrom contextlib import contextmanager\nfrom shutil import rmtree\nfrom tempfile import NamedTemporaryFile\n\nimport pytest\n\nfrom piptools.cache import CorruptCacheError, DependencyCache, read_cache_file\n\n\n@contextmanager\ndef _read_cache_file_helper(to_write):\n    \"\"\"\n    On enter, create the file with the given string, and then yield its path.\n    On exit, delete that file.\n\n    :param str to_write: the content to write to the file\n    :yield: the path to the temporary file\n    \"\"\"\n    # Create the file and write to it\n    cache_file = NamedTemporaryFile(mode=\"w\", delete=False)\n    try:\n        cache_file.write(to_write)\n        cache_file.close()\n\n        # Yield the path to the file\n        yield cache_file.name\n\n    finally:\n        # Delete the file on exit\n        os.remove(cache_file.name)\n\n\ndef test_read_cache_file_not_json():\n    \"\"\"\n    A cache file that's not JSON should throw a corrupt cache error.\n    \"\"\"\n    with _read_cache_file_helper(\"not json\") as cache_file_name:\n        with pytest.raises(\n            CorruptCacheError,\n            match=\"The dependency cache seems to have been corrupted.\",\n        ):\n            read_cache_file(cache_file_name)\n\n\ndef test_read_cache_file_wrong_format():\n    \"\"\"\n    A cache file with a wrong \"__format__\" value should throw an assertion error.\n    \"\"\"\n    with _read_cache_file_helper('{\"__format__\": 2}') as cache_file_name:\n        with pytest.raises(ValueError, match=r\"^Unknown cache file format$\"):\n            read_cache_file(cache_file_name)\n\n\ndef test_read_cache_file_successful():\n    \"\"\"\n    A good cache file.\n    \"\"\"\n    with _read_cache_file_helper(\n        '{\"__format__\": 1, \"dependencies\": \"success\"}'\n    ) as cache_file_name:\n        assert \"success\" == read_cache_file(cache_file_name)\n\n\ndef test_read_cache_does_not_exist(tmpdir):\n    cache = DependencyCache(cache_dir=tmpdir)\n    assert cache.cache == {}\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"os.fchmod() not available on Windows\"\n)\ndef test_read_cache_permission_error(tmpdir):\n    cache = DependencyCache(cache_dir=tmpdir)\n    with open(cache._cache_file, \"w\") as fp:\n        os.fchmod(fp.fileno(), 0o000)\n    with pytest.raises(IOError, match=\"Permission denied\"):\n        cache.cache\n\n\ndef test_reverse_dependencies(from_line, tmpdir):\n    # Create a cache object. The keys are packages, and the values are lists\n    # of packages on which the keys depend.\n    cache = DependencyCache(cache_dir=tmpdir)\n    cache[from_line(\"top==1.2\")] = [\"middle>=0.3\", \"bottom>=5.1.2\"]\n    cache[from_line(\"top[xtra]==1.2\")] = [\"middle>=0.3\", \"bottom>=5.1.2\", \"bonus==0.4\"]\n    cache[from_line(\"middle==0.4\")] = [\"bottom<6\"]\n    cache[from_line(\"bottom==5.3.5\")] = []\n    cache[from_line(\"bonus==0.4\")] = []\n\n    # In this case, we're using top 1.2 without an extra, so the \"bonus\" package\n    # is not depended upon.\n    reversed_no_extra = cache.reverse_dependencies(\n        [\n            from_line(\"top==1.2\"),\n            from_line(\"middle==0.4\"),\n            from_line(\"bottom==5.3.5\"),\n            from_line(\"bonus==0.4\"),\n        ]\n    )\n    assert reversed_no_extra == {\"middle\": {\"top\"}, \"bottom\": {\"middle\", \"top\"}}\n\n    # Now we're using top 1.2 with the \"xtra\" extra, so it depends\n    # on the \"bonus\" package.\n    reversed_extra = cache.reverse_dependencies(\n        [\n            from_line(\"top[xtra]==1.2\"),\n            from_line(\"middle==0.4\"),\n            from_line(\"bottom==5.3.5\"),\n            from_line(\"bonus==0.4\"),\n        ]\n    )\n    assert reversed_extra == {\n        \"middle\": {\"top\"},\n        \"bottom\": {\"middle\", \"top\"},\n        \"bonus\": {\"top\"},\n    }\n\n    # Clean up our temp directory\n    rmtree(tmpdir)\n"
  },
  {
    "path": "tests/test_circular_imports.py",
    "content": "\"\"\"Tests for circular imports in all local packages and modules.\n\nThis ensures all internal packages can be imported right away without\nany need to import some other module before doing so.\n\nThis module is based on an idea that pytest uses for self-testing:\n* https://github.com/aio-libs/aiohttp/blob/91108c9/tests/test_circular_imports.py\n* https://github.com/sanitizers/octomachinery/blob/be18b54/tests/circular_imports_test.py\n* https://github.com/pytest-dev/pytest/blob/d18c75b/testing/test_meta.py\n* https://twitter.com/codewithanthony/status/1229445110510735361\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport pkgutil\nimport subprocess\nimport sys\nfrom collections.abc import Iterator\nfrom itertools import chain\nfrom pathlib import Path\nfrom types import ModuleType\n\nimport pytest\n\nimport piptools\nfrom piptools._internal import _pip_api\n\n\ndef _find_all_importables(pkg: ModuleType) -> list[str]:\n    \"\"\"Find all importables in the project.\n\n    Return them in order.\n    \"\"\"\n    return sorted(\n        set(\n            chain.from_iterable(\n                _discover_path_importables(Path(p), pkg.__name__) for p in pkg.__path__\n            ),\n        ),\n    )\n\n\ndef _discover_path_importables(pkg_pth: Path, pkg_name: str) -> Iterator[str]:\n    \"\"\"Yield all importables under a given path and package.\"\"\"\n    for dir_path, _d, file_names in os.walk(pkg_pth):\n        pkg_dir_path = Path(dir_path)\n\n        if pkg_dir_path.parts[-1] == \"__pycache__\":\n            continue\n\n        if all(Path(_).suffix != \".py\" for _ in file_names):  # pragma: no cover\n            continue\n\n        rel_pt = pkg_dir_path.relative_to(pkg_pth)\n        pkg_pref = \".\".join((pkg_name,) + rel_pt.parts)\n        yield from (\n            pkg_path\n            for _, pkg_path, _ in pkgutil.walk_packages(\n                (str(pkg_dir_path),),\n                prefix=f\"{pkg_pref}.\",\n            )\n        )\n\n\ndef _allowed_deprecation_warning_filters() -> list[str]:\n    \"\"\"\n    Yield filters which allow for deprecation warnings based on the current\n    test environment.\n    \"\"\"\n    # note that we can't use regex syntax in filters as of yet, only literals\n    # https://github.com/python/cpython/pull/138149 allows regex usage, but is not\n    # yet supported on all Python versions we support\n    flags: list[str] = []\n    if _pip_api.PIP_VERSION_MAJOR_MINOR < (25, 3):\n        flags.extend(\n            (\"-W\", \"ignore:pkg_resources is deprecated as an API.:DeprecationWarning:\")\n        )\n    if _pip_api.PIP_VERSION_MAJOR_MINOR <= (22, 2):\n        flags.extend(\n            (\n                \"-W\",\n                (\n                    \"ignore:path is deprecated. Use files() instead.\"\n                    \":DeprecationWarning:\"\n                ),\n                \"-W\",\n                (\n                    \"ignore:Creating a LegacyVersion has been deprecated \"\n                    \"and will be removed in the next major release\"\n                    \":DeprecationWarning:\"\n                ),\n            )\n        )\n    return flags\n\n\n@pytest.mark.parametrize(\"import_path\", _find_all_importables(piptools))\ndef test_no_warnings(import_path: str) -> None:\n    \"\"\"Verify that each importable name can be independently imported.\n\n    This is seeking for any import errors including ones caused\n    by circular imports.\n    \"\"\"\n    import_statement = f\"import {import_path!s}\"\n    # On lower pip versions, we need to allow certain deprecation warnings.\n    flags = (\"-W\", \"error\", *_allowed_deprecation_warning_filters())\n    command = (sys.executable, *flags, \"-c\", import_statement)\n\n    subprocess.check_call(command)\n"
  },
  {
    "path": "tests/test_cli_compile.py",
    "content": "from __future__ import annotations\n\nimport dataclasses\nimport hashlib\nimport os\nimport pathlib\nimport re\nimport shlex\nimport shutil\nimport subprocess\nimport sys\nimport typing as _t\nfrom textwrap import dedent\nfrom unittest import mock\nfrom unittest.mock import MagicMock\n\nimport pytest\nfrom pip._internal.network.session import PipSession\nfrom pip._internal.req.constructors import install_req_from_line\nfrom pip._internal.utils.hashes import FAVORITE_HASH\nfrom pip._internal.utils.urls import path_to_url\nfrom pip._vendor.packaging.version import Version\n\nfrom piptools._compat import tempfile_compat\nfrom piptools._internal import _pip_api\nfrom piptools.build import ProjectMetadata\nfrom piptools.scripts.compile import cli\nfrom piptools.utils import COMPILE_EXCLUDE_OPTIONS\n\nfrom .constants import MINIMAL_WHEELS_PATH, PACKAGES_PATH\n\nlegacy_resolver_only = pytest.mark.parametrize(\n    \"current_resolver\",\n    (\"legacy\",),\n    indirect=(\"current_resolver\",),\n)\n\nbacktracking_resolver_only = pytest.mark.parametrize(\n    \"current_resolver\",\n    (\"backtracking\",),\n    indirect=(\"current_resolver\",),\n)\n\n\nskip_if_pip_does_not_support_editables_in_constraints = pytest.mark.skipif(\n    _pip_api.PIP_VERSION_MAJOR_MINOR >= (26, 0),\n    reason=\"pip v26.0 and later does not support editables in constraints files\",\n)\n\n\n@pytest.fixture(scope=\"session\")\ndef pip_produces_absolute_paths():\n    # in pip v24.3, new normalization will occur because `comes_from` started\n    # to be normalized to abspaths\n    return _pip_api.PIP_VERSION_MAJOR_MINOR >= (24, 3)\n\n\n@dataclasses.dataclass\nclass FileCollectionParam:\n    \"\"\"\n    A small data-builder for setting up files in a tmp dir.\n\n    Contains a name for use as the ID in parametrized tests and contents.\n    'contents' maps from subpaths in the tmp dir to file content or callables\n    which produce file content given the tmp dir.\n    \"\"\"\n\n    # the name for the collection of files\n    name: str = \"<unnamed test file collection>\"\n    # static or computed contents\n    contents: dict[str, str | _t.Callable[[pathlib.Path], str]] = dataclasses.field(\n        default_factory=dict\n    )\n\n    def __str__(self) -> str:\n        return self.name\n\n    def populate(self, tmp_path: pathlib.Path) -> None:\n        \"\"\"Populate the tmp dir with file contents.\"\"\"\n        for path_str, content in self.contents.items():\n            path = tmp_path / path_str\n            path.parent.mkdir(exist_ok=True, parents=True)\n            if isinstance(content, str):\n                path.write_text(content)\n            else:\n                path.write_text(content(tmp_path))\n\n    def get_path_to(self, filename: str) -> str:\n        \"\"\"Given a filename, find the (first) path to that filename in the contents.\"\"\"\n        return next(\n            stub_file_path\n            for stub_file_path in self.contents\n            if (stub_file_path == filename) or stub_file_path.endswith(f\"/{filename}\")\n        )\n\n\n@dataclasses.dataclass\nclass PackageVersionParam:\n    \"\"\"\n    An object for writing ergonomic test parameters.\n\n    This describes a published package with a version.\n    \"\"\"\n\n    # package name\n    name: str\n    # (unparsed) version string\n    version: str\n    # a description of this version (for use in ids)\n    description: str\n\n    def __str__(self) -> str:\n        return f\"{self.name}-{self.version}-{self.description}\"\n\n    def as_req(self) -> str:\n        return f\"{self.name}=={self.version}\"\n\n\n@pytest.fixture(\n    autouse=True,\n    params=[\n        pytest.param(\"legacy\", id=\"legacy resolver\"),\n        pytest.param(\"backtracking\", id=\"backtracking resolver\"),\n    ],\n)\ndef current_resolver(request, monkeypatch):\n    # Hide --resolver option from pip-compile header, so that we don't have to\n    # inject it every time to tests outputs.\n    exclude_options = COMPILE_EXCLUDE_OPTIONS | {\"--resolver\"}\n    monkeypatch.setattr(\"piptools.utils.COMPILE_EXCLUDE_OPTIONS\", exclude_options)\n\n    # Setup given resolver name\n    resolver_name = request.param\n    monkeypatch.setenv(\"PIP_TOOLS_RESOLVER\", resolver_name)\n    return resolver_name\n\n\n@pytest.fixture(autouse=True)\ndef _temp_dep_cache(tmpdir, monkeypatch):\n    monkeypatch.setenv(\"PIP_TOOLS_CACHE_DIR\", str(tmpdir / \"cache\"))\n\n\ndef test_default_pip_conf_read(pip_with_index_conf, runner):\n    # preconditions\n    with open(\"requirements.in\", \"w\"):\n        pass\n    out = runner.invoke(cli, [\"-v\"])\n\n    # check that we have our index-url as specified in pip.conf\n    assert \"Using indexes:\\n  http://example.com\" in out.stderr\n    assert \"--index-url http://example.com\" in out.stderr\n\n\ndef test_command_line_overrides_pip_conf(pip_with_index_conf, runner):\n    # preconditions\n    with open(\"requirements.in\", \"w\"):\n        pass\n    out = runner.invoke(cli, [\"-v\", \"-i\", \"http://override.com\"])\n\n    # check that we have our index-url as specified in pip.conf\n    assert \"Using indexes:\\n  http://override.com\" in out.stderr\n\n\n@pytest.mark.network\n@pytest.mark.parametrize(\n    (\"install_requires\", \"expected_output\"),\n    (\n        pytest.param(\"small-fake-a==0.1\", \"small-fake-a==0.1\", id=\"regular\"),\n        pytest.param(\n            \"pip-tools @ https://github.com/jazzband/pip-tools/archive/7d86c8d3.zip\",\n            \"pip-tools @ https://github.com/jazzband/pip-tools/archive/7d86c8d3.zip\",\n            id=\"zip URL\",\n        ),\n        pytest.param(\n            \"pip-tools @ git+https://github.com/jazzband/pip-tools@7d86c8d3\",\n            \"pip-tools @ git+https://github.com/jazzband/pip-tools@7d86c8d3\",\n            id=\"scm URL\",\n        ),\n        pytest.param(\n            \"pip-tools @ https://files.pythonhosted.org/packages/06/96/\"\n            \"89872db07ae70770fba97205b0737c17ef013d0d1c790\"\n            \"899c16bb8bac419/pip_tools-3.6.1-py2.py3-none-any.whl\",\n            \"pip-tools @ https://files.pythonhosted.org/packages/06/96/\"\n            \"89872db07ae70770fba97205b0737c17ef013d0d1c790\"\n            \"899c16bb8bac419/pip_tools-3.6.1-py2.py3-none-any.whl\",\n            id=\"wheel URL\",\n        ),\n    ),\n)\ndef test_command_line_setuptools_read(\n    runner, make_package, minimal_wheels_path, install_requires, expected_output\n):\n    package_dir = make_package(\n        name=\"fake-setuptools-a\",\n        install_requires=(install_requires,),\n    )\n\n    out = runner.invoke(\n        cli,\n        (\n            str(package_dir / \"setup.py\"),\n            \"--find-links\",\n            minimal_wheels_path.as_posix(),\n            \"--no-build-isolation\",\n        ),\n    )\n    assert out.exit_code == 0\n\n    # check that pip-compile generated a configuration file\n    output_file = package_dir / \"requirements.txt\"\n    assert output_file.exists()\n\n    # The package version must NOT be updated in the output file\n    assert expected_output in output_file.read_text().splitlines()\n\n\n@pytest.mark.network\n@pytest.mark.parametrize(\n    (\"options\", \"expected_output_file\"),\n    (\n        # For the `pip-compile` output file should be \"requirements.txt\"\n        ([], \"requirements.txt\"),\n        # For the `pip-compile --output-file=output.txt`\n        # output file should be \"output.txt\"\n        ([\"--output-file\", \"output.txt\"], \"output.txt\"),\n        # For the `pip-compile setup.py` output file should be \"requirements.txt\"\n        ([\"setup.py\"], \"requirements.txt\"),\n        # For the `pip-compile setup.py --output-file=output.txt`\n        # output file should be \"output.txt\"\n        ([\"setup.py\", \"--output-file\", \"output.txt\"], \"output.txt\"),\n    ),\n)\ndef test_command_line_setuptools_output_file(runner, options, expected_output_file):\n    \"\"\"\n    Test the output files for setup.py as a requirement file.\n    \"\"\"\n\n    with open(\"setup.py\", \"w\") as package:\n        package.write(dedent(\"\"\"\\\n                from setuptools import setup\n                setup(install_requires=[])\n                \"\"\"))\n\n    out = runner.invoke(cli, [\"--no-build-isolation\"] + options)\n    assert out.exit_code == 0\n    assert os.path.exists(expected_output_file)\n\n\n@pytest.mark.network\ndef test_command_line_setuptools_nested_output_file(tmpdir, runner):\n    \"\"\"\n    Test the output file for setup.py in nested folder as a requirement file.\n    \"\"\"\n    proj_dir = tmpdir.mkdir(\"proj\")\n\n    with open(str(proj_dir / \"setup.py\"), \"w\") as package:\n        package.write(dedent(\"\"\"\\\n                from setuptools import setup\n                setup(install_requires=[])\n                \"\"\"))\n\n    out = runner.invoke(cli, [str(proj_dir / \"setup.py\"), \"--no-build-isolation\"])\n    assert out.exit_code == 0\n    assert (proj_dir / \"requirements.txt\").exists()\n\n\n@pytest.mark.network\ndef test_setuptools_preserves_environment_markers(\n    runner, make_package, make_wheel, make_pip_conf, tmpdir\n):\n    make_pip_conf(dedent(\"\"\"\\\n            [global]\n            disable-pip-version-check = True\n            \"\"\"))\n\n    dists_dir = tmpdir / \"dists\"\n\n    foo_dir = make_package(name=\"foo\", version=\"1.0\")\n    make_wheel(foo_dir, dists_dir)\n\n    bar_dir = make_package(\n        name=\"bar\", version=\"2.0\", install_requires=['foo ; python_version >= \"1\"']\n    )\n    out = runner.invoke(\n        cli,\n        [\n            str(bar_dir / \"setup.py\"),\n            \"--output-file\",\n            \"-\",\n            \"--no-header\",\n            \"--no-annotate\",\n            \"--no-emit-find-links\",\n            \"--no-build-isolation\",\n            \"--find-links\",\n            str(dists_dir),\n        ],\n    )\n\n    assert out.exit_code == 0, out.stderr\n    assert out.stdout == 'foo==1.0 ; python_version >= \"1\"\\n'\n\n\ndef test_no_index_option(runner, tmp_path):\n    req_in = tmp_path / \"requirements.in\"\n    req_in.touch()\n\n    out = runner.invoke(cli, [req_in.as_posix(), \"--no-index\", \"--verbose\"])\n\n    assert out.exit_code == 0\n    assert \"Ignoring indexes.\" in out.stderr\n\n\ndef test_find_links_option(runner):\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"-f ./libs3\")\n\n    out = runner.invoke(cli, [\"-v\", \"-f\", \"./libs1\", \"-f\", \"./libs2\"])\n\n    # Check that find-links has been passed to pip\n    assert \"Using links:\\n  ./libs1\\n  ./libs2\\n  ./libs3\\n\" in out.stderr\n\n    # Check that find-links has been written to a requirements.txt\n    with open(\"requirements.txt\") as req_txt:\n        assert (\n            \"--find-links ./libs1\\n--find-links ./libs2\\n--find-links ./libs3\\n\"\n            in req_txt.read()\n        )\n\n\ndef test_find_links_envvar(monkeypatch, runner):\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"-f ./libs3\")\n\n    monkeypatch.setenv(\"PIP_FIND_LINKS\", \"./libs1 ./libs2\")\n    out = runner.invoke(cli, [\"-v\"])\n\n    # Check that find-links has been passed to pip\n    assert \"Using links:\\n  ./libs1\\n  ./libs2\\n  ./libs3\\n\" in out.stderr\n\n    # Check that find-links has been written to a requirements.txt\n    with open(\"requirements.txt\") as req_txt:\n        assert (\n            \"--find-links ./libs1\\n--find-links ./libs2\\n--find-links ./libs3\\n\"\n            in req_txt.read()\n        )\n\n\ndef test_extra_index_option(pip_with_index_conf, runner):\n    with open(\"requirements.in\", \"w\"):\n        pass\n    out = runner.invoke(\n        cli,\n        [\n            \"-v\",\n            \"--extra-index-url\",\n            \"http://extraindex1.com\",\n            \"--extra-index-url\",\n            \"http://extraindex2.com\",\n        ],\n    )\n    assert (\n        \"Using indexes:\\n\"\n        \"  http://example.com\\n\"\n        \"  http://extraindex1.com\\n\"\n        \"  http://extraindex2.com\" in out.stderr\n    )\n    assert (\n        \"--index-url http://example.com\\n\"\n        \"--extra-index-url http://extraindex1.com\\n\"\n        \"--extra-index-url http://extraindex2.com\" in out.stderr\n    )\n\n\ndef test_extra_index_envvar(monkeypatch, runner):\n    with open(\"requirements.in\", \"w\"):\n        pass\n\n    monkeypatch.setenv(\"PIP_INDEX_URL\", \"http://example.com\")\n    monkeypatch.setenv(\n        \"PIP_EXTRA_INDEX_URL\", \"http://extraindex1.com http://extraindex2.com\"\n    )\n    out = runner.invoke(cli, [\"-v\"])\n    assert (\n        \"Using indexes:\\n\"\n        \"  http://example.com\\n\"\n        \"  http://extraindex1.com\\n\"\n        \"  http://extraindex2.com\" in out.stderr\n    )\n    assert (\n        \"--index-url http://example.com\\n\"\n        \"--extra-index-url http://extraindex1.com\\n\"\n        \"--extra-index-url http://extraindex2.com\" in out.stderr\n    )\n\n\n@pytest.mark.parametrize(\"option\", (\"--extra-index-url\", \"--find-links\"))\ndef test_redacted_urls_in_verbose_output(runner, option):\n    \"\"\"\n    Test that URLs with sensitive data don't leak to the output.\n    \"\"\"\n    with open(\"requirements.in\", \"w\"):\n        pass\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--no-header\",\n            \"--no-emit-index-url\",\n            \"--no-emit-find-links\",\n            \"--verbose\",\n            option,\n            \"http://username:password@example.com\",\n        ],\n    )\n\n    assert \"http://username:****@example.com\" in out.stderr\n    assert \"password\" not in out.stderr\n\n\ndef test_trusted_host_option(pip_conf, runner):\n    with open(\"requirements.in\", \"w\"):\n        pass\n    out = runner.invoke(\n        cli, [\"-v\", \"--trusted-host\", \"example.com\", \"--trusted-host\", \"example2.com\"]\n    )\n    assert \"--trusted-host example.com\\n--trusted-host example2.com\\n\" in out.stderr\n\n\ndef test_trusted_host_envvar(monkeypatch, pip_conf, runner):\n    with open(\"requirements.in\", \"w\"):\n        pass\n    monkeypatch.setenv(\"PIP_TRUSTED_HOST\", \"example.com example2.com\")\n    out = runner.invoke(cli, [\"-v\"])\n    assert \"--trusted-host example.com\\n--trusted-host example2.com\\n\" in out.stderr\n\n\n@pytest.mark.parametrize(\n    \"options\",\n    (\n        pytest.param(\n            [\"--trusted-host\", \"example.com\", \"--no-emit-trusted-host\"],\n            id=\"trusted host\",\n        ),\n        pytest.param(\n            [\"--find-links\", \"wheels\", \"--no-emit-find-links\"], id=\"find links\"\n        ),\n        pytest.param(\n            [\"--index-url\", \"https://index-url\", \"--no-emit-index-url\"], id=\"index url\"\n        ),\n    ),\n)\ndef test_all_no_emit_options(runner, options):\n    with open(\"requirements.in\", \"w\"):\n        pass\n    out = runner.invoke(\n        cli, [\"--output-file\", \"-\", \"--no-header\", \"--strip-extras\", *options]\n    )\n    assert out.stdout.strip().splitlines() == []\n\n\n@pytest.mark.parametrize(\n    (\"option\", \"expected_output\"),\n    (\n        pytest.param(\n            \"--emit-index-url\", [\"--index-url https://index-url\"], id=\"index url\"\n        ),\n        pytest.param(\"--no-emit-index-url\", [], id=\"no index\"),\n    ),\n)\ndef test_emit_index_url_option(runner, option, expected_output):\n    with open(\"requirements.in\", \"w\"):\n        pass\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--output-file\",\n            \"-\",\n            \"--quiet\",\n            \"--no-header\",\n            \"--index-url\",\n            \"https://index-url\",\n            option,\n        ],\n    )\n\n    assert out.stdout.strip().splitlines() == expected_output\n\n\n@pytest.mark.network\ndef test_realistic_complex_sub_dependencies(runner, tmp_path):\n    wheels_dir = tmp_path / \"wheels\"\n    wheels_dir.mkdir()\n\n    # make a temporary wheel of a fake package\n    subprocess.run(\n        [\n            \"pip\",\n            \"wheel\",\n            \"--no-deps\",\n            \"-w\",\n            wheels_dir,\n            os.path.join(PACKAGES_PATH, \"fake_with_deps\", \".\"),\n        ],\n        check=True,\n    )\n\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"fake_with_deps\")  # require fake package\n\n    out = runner.invoke(cli, [\"-n\", \"--rebuild\", \"-f\", wheels_dir.as_posix()])\n\n    assert out.exit_code == 0\n\n\ndef test_run_as_module_compile():\n    \"\"\"piptools can be run as ``python -m piptools ...``.\"\"\"\n\n    result = subprocess.run(\n        [sys.executable, \"-m\", \"piptools\", \"compile\", \"--help\"],\n        stdout=subprocess.PIPE,\n        check=True,\n    )\n\n    # Should have run pip-compile successfully.\n    assert result.stdout.startswith(b\"Usage:\")\n    assert b\"Compile requirements.txt from source files\" in result.stdout\n\n\ndef test_compile_help_opt_supports_short_and_long_flag(runner):\n    shortflag_result = runner.invoke(cli, [\"-h\"])\n    longflag_result = runner.invoke(cli, [\"--help\"])\n    assert shortflag_result.exit_code == 0\n    assert longflag_result.exit_code == 0\n\n    assert shortflag_result.stdout.startswith(\"Usage:\")\n    assert longflag_result.stdout.startswith(\"Usage:\")\n    assert shortflag_result.stdout == longflag_result.stdout\n\n\ndef test_compile_help_opt_shows_examples_section(runner):\n    result = runner.invoke(cli, [\"-h\"])\n    assert result.exit_code == 0\n    assert result.stdout.startswith(\"Usage:\")\n\n    # not only should there be an `Examples` section in the output, but it should have\n    # no preceding whitespace where it is shown\n    assert \"\\nExamples:\\n\" in result.stdout\n\n\ndef test_editable_package(pip_conf, runner):\n    \"\"\"piptools can compile an editable\"\"\"\n    fake_package_dir = os.path.join(PACKAGES_PATH, \"small_fake_with_deps\")\n    fake_package_dir = path_to_url(fake_package_dir)\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"-e \" + fake_package_dir)  # require editable fake package\n\n    out = runner.invoke(cli, [\"-n\"])\n\n    assert out.exit_code == 0\n    assert fake_package_dir in out.stderr\n    assert \"small-fake-a==0.1\" in out.stderr\n\n\ndef test_editable_package_without_non_editable_duplicate(pip_conf, runner):\n    \"\"\"\n    piptools keeps editable requirement,\n    without also adding a duplicate \"non-editable\" requirement variation\n    \"\"\"\n    fake_package_dir = os.path.join(PACKAGES_PATH, \"small_fake_a\")\n    fake_package_dir = path_to_url(fake_package_dir)\n    with open(\"requirements.in\", \"w\") as req_in:\n        # small_fake_with_unpinned_deps also requires small_fake_a\n        req_in.write(\n            \"-e \"\n            + fake_package_dir\n            + \"\\nsmall_fake_with_unpinned_deps\"  # require editable fake package\n        )\n\n    out = runner.invoke(cli, [\"-n\"])\n\n    assert out.exit_code == 0\n    assert fake_package_dir in out.stderr\n    # Shouldn't include a non-editable small-fake-a==<version>.\n    assert \"small-fake-a==\" not in out.stderr\n\n\n@legacy_resolver_only\n@skip_if_pip_does_not_support_editables_in_constraints\ndef test_editable_package_constraint_without_non_editable_duplicate(pip_conf, runner):\n    \"\"\"\n    piptools keeps editable constraint,\n    without also adding a duplicate \"non-editable\" requirement variation\n    \"\"\"\n    fake_package_dir = os.path.join(PACKAGES_PATH, \"small_fake_a\")\n    fake_package_dir = path_to_url(fake_package_dir)\n    with open(\"constraints.txt\", \"w\") as constraints:\n        constraints.write(\"-e \" + fake_package_dir)  # require editable fake package\n\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\n            \"-c constraints.txt\"  # require editable fake package\n            \"\\nsmall_fake_with_unpinned_deps\"  # This one also requires small_fake_a\n        )\n\n    out = runner.invoke(cli, [\"--output-file\", \"-\", \"--quiet\"])\n\n    assert out.exit_code == 0\n    assert fake_package_dir in out.stdout\n    # Shouldn't include a non-editable small-fake-a==<version>.\n    assert \"small-fake-a==\" not in out.stdout\n\n\n@legacy_resolver_only\n@skip_if_pip_does_not_support_editables_in_constraints\n@pytest.mark.parametrize(\"req_editable\", ((True,), (False,)))\ndef test_editable_package_in_constraints(pip_conf, runner, req_editable):\n    \"\"\"\n    piptools can compile an editable that appears in both primary requirements\n    and constraints\n    \"\"\"\n    fake_package_dir = os.path.join(PACKAGES_PATH, \"small_fake_with_deps\")\n    fake_package_dir = path_to_url(fake_package_dir)\n\n    with open(\"constraints.txt\", \"w\") as constraints_in:\n        constraints_in.write(\"-e \" + fake_package_dir)\n\n    with open(\"requirements.in\", \"w\") as req_in:\n        prefix = \"-e \" if req_editable else \"\"\n        req_in.write(prefix + fake_package_dir + \"\\n-c constraints.txt\")\n\n    out = runner.invoke(cli, [\"-n\"])\n\n    assert out.exit_code == 0\n    assert fake_package_dir in out.stderr\n    assert \"small-fake-a==0.1\" in out.stderr\n\n\n@pytest.mark.network\ndef test_editable_package_vcs(runner):\n    vcs_package = (\n        \"git+https://github.com/jazzband/pip-tools@\"\n        \"f97e62ecb0d9b70965c8eff952c001d8e2722e94\"\n        \"#egg=pip-tools\"\n    )\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"-e \" + vcs_package)\n    out = runner.invoke(cli, [\"-n\", \"--rebuild\"])\n    assert out.exit_code == 0\n    assert vcs_package in out.stderr\n    assert \"click\" in out.stderr  # dependency of pip-tools\n\n\n@pytest.mark.network\ndef test_compile_cached_vcs_package(runner, venv):\n    \"\"\"\n    Test pip-compile doesn't write local paths for cached wheels of VCS packages.\n\n    Regression test for issue GH-1647.\n    \"\"\"\n    vcs_package = (\n        \"typing-extensions @ git+https://github.com/python/typing_extensions@\"\n        \"9c0759a260fe126210a1e2026720000a3c40a919\"\n    )\n    vcs_wheel_prefix = \"typing_extensions-4.3.0-py3\"\n\n    # Install and cache VCS package.\n    subprocess.run(\n        [os.fspath(venv / \"python\"), \"-m\" \"pip\", \"install\", vcs_package],\n        check=True,\n    )\n    assert (\n        vcs_wheel_prefix\n        in subprocess.run(\n            [\n                sys.executable,\n                \"-m\" \"pip\",\n                \"cache\",\n                \"list\",\n                \"--format=abspath\",\n                vcs_wheel_prefix,\n            ],\n            check=True,\n            capture_output=True,\n            text=True,\n        ).stdout\n    )\n\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(vcs_package)\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--output-file\",\n            \"-\",\n            \"--quiet\",\n            \"--no-header\",\n            \"--no-emit-options\",\n            \"--no-annotate\",\n            \"--no-build-isolation\",\n        ],\n    )\n\n    assert out.exit_code == 0, out\n    assert vcs_package == out.stdout.strip()\n\n\n@legacy_resolver_only\ndef test_locally_available_editable_package_is_not_archived_in_cache_dir(\n    pip_conf, tmpdir, runner\n):\n    \"\"\"\n    piptools will not create an archive for a locally available editable requirement\n    \"\"\"\n    cache_dir = tmpdir.mkdir(\"cache_dir\")\n\n    fake_package_dir = os.path.join(PACKAGES_PATH, \"small_fake_with_deps\")\n    fake_package_dir = path_to_url(fake_package_dir)\n\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"-e \" + fake_package_dir)  # require editable fake package\n\n    out = runner.invoke(cli, [\"-n\", \"--rebuild\", \"--cache-dir\", str(cache_dir)])\n\n    assert out.exit_code == 0\n    assert fake_package_dir in out.stderr\n    assert \"small-fake-a==0.1\" in out.stderr\n\n    # we should not find any archived file in {cache_dir}/pkgs\n    assert not os.listdir(os.path.join(str(cache_dir), \"pkgs\"))\n\n\n@pytest.mark.parametrize(\n    (\"line\", \"dependency\"),\n    (\n        # use pip-tools version prior to its use of setuptools_scm,\n        # which is incompatible with https: install\n        pytest.param(\n            \"https://github.com/jazzband/pip-tools/archive/\"\n            \"7d86c8d3ecd1faa6be11c7ddc6b29a30ffd1dae3.zip\",\n            \"\\nclick==\",\n            id=\"Zip URL\",\n        ),\n        pytest.param(\n            \"git+https://github.com/jazzband/pip-tools@\"\n            \"7d86c8d3ecd1faa6be11c7ddc6b29a30ffd1dae3\",\n            \"\\nclick==\",\n            id=\"VCS URL\",\n        ),\n        pytest.param(\n            \"https://files.pythonhosted.org/packages/06/96/\"\n            \"89872db07ae70770fba97205b0737c17ef013d0d1c790\"\n            \"899c16bb8bac419/pip_tools-3.6.1-py2.py3-none-any.whl\",\n            \"\\nclick==\",\n            id=\"Wheel URL\",\n        ),\n        pytest.param(\n            \"pytest-django @ git+https://github.com/pytest-dev/pytest-django\"\n            \"@21492afc88a19d4ca01cd0ac392a5325b14f95c7\"\n            \"#egg=pytest-django\",\n            \"pytest-django @ git+https://github.com/pytest-dev/pytest-django\"\n            \"@21492afc88a19d4ca01cd0ac392a5325b14f95c7\",\n            id=\"VCS with direct reference and egg\",\n        ),\n    ),\n)\n@pytest.mark.parametrize(\"generate_hashes\", ((True,), (False,)))\n@pytest.mark.network\ndef test_url_package(runner, line, dependency, generate_hashes):\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(line)\n    out = runner.invoke(\n        cli,\n        [\"-n\", \"--rebuild\", \"--no-build-isolation\"]\n        + ([\"--generate-hashes\"] if generate_hashes else []),\n    )\n    assert out.exit_code == 0\n    assert dependency in out.stderr\n\n\n@pytest.mark.parametrize(\n    (\"line\", \"dependency\", \"rewritten_line\"),\n    (\n        pytest.param(\n            path_to_url(\n                os.path.join(\n                    MINIMAL_WHEELS_PATH, \"small_fake_with_deps-0.1-py2.py3-none-any.whl\"\n                )\n            ),\n            \"\\nsmall-fake-a==0.1\",\n            None,\n            id=\"Wheel URI\",\n        ),\n        pytest.param(\n            path_to_url(os.path.join(PACKAGES_PATH, \"small_fake_with_deps\")),\n            \"\\nsmall-fake-a==0.1\",\n            None,\n            id=\"Local project URI\",\n        ),\n        pytest.param(\n            os.path.join(\n                MINIMAL_WHEELS_PATH, \"small_fake_with_deps-0.1-py2.py3-none-any.whl\"\n            ),\n            \"\\nsmall-fake-a==0.1\",\n            path_to_url(\n                os.path.join(\n                    MINIMAL_WHEELS_PATH, \"small_fake_with_deps-0.1-py2.py3-none-any.whl\"\n                )\n            ),\n            id=\"Bare path to file URI\",\n        ),\n        pytest.param(\n            os.path.join(\n                MINIMAL_WHEELS_PATH, \"small_fake_with_deps-0.1-py2.py3-none-any.whl\"\n            ),\n            \"\\nsmall-fake-with-deps @ \"\n            + path_to_url(\n                os.path.join(\n                    MINIMAL_WHEELS_PATH, \"small_fake_with_deps-0.1-py2.py3-none-any.whl\"\n                )\n            ),\n            \"\\nsmall-fake-with-deps @ \"\n            + path_to_url(\n                os.path.join(\n                    MINIMAL_WHEELS_PATH, \"small_fake_with_deps-0.1-py2.py3-none-any.whl\"\n                )\n            ),\n            id=\"Local project with absolute URI\",\n        ),\n        pytest.param(\n            path_to_url(os.path.join(PACKAGES_PATH, \"small_fake_with_subdir\"))\n            + \"#subdirectory=subdir&egg=small-fake-a\",\n            \"small-fake-a @ \"\n            + path_to_url(os.path.join(PACKAGES_PATH, \"small_fake_with_subdir\"))\n            + \"#subdirectory=subdir\",\n            \"small-fake-a @ \"\n            + path_to_url(os.path.join(PACKAGES_PATH, \"small_fake_with_subdir\"))\n            + \"#subdirectory=subdir\",\n            id=\"Local project with subdirectory\",\n        ),\n    ),\n)\n@pytest.mark.parametrize(\"generate_hashes\", ((True,), (False,)))\ndef test_local_file_uri_package(\n    pip_conf, runner, line, dependency, rewritten_line, generate_hashes\n):\n    if rewritten_line is None:\n        rewritten_line = line\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(line)\n    out = runner.invoke(\n        cli, [\"-n\", \"--rebuild\"] + ([\"--generate-hashes\"] if generate_hashes else [])\n    )\n    assert out.exit_code == 0\n    assert rewritten_line in out.stderr\n    assert dependency in out.stderr\n\n\ndef test_relative_file_uri_package(pip_conf, runner):\n    # Copy wheel into temp dir\n    shutil.copy(\n        os.path.join(\n            MINIMAL_WHEELS_PATH, \"small_fake_with_deps-0.1-py2.py3-none-any.whl\"\n        ),\n        \".\",\n    )\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"file:small_fake_with_deps-0.1-py2.py3-none-any.whl\")\n    out = runner.invoke(cli, [\"-n\", \"--rebuild\"])\n    assert out.exit_code == 0\n    assert \"file:small_fake_with_deps-0.1-py2.py3-none-any.whl\" in out.stderr\n\n\ndef test_direct_reference_with_extras(runner):\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\n            \"pip-tools[testing,coverage] @ git+https://github.com/jazzband/pip-tools@6.2.0\"\n        )\n    out = runner.invoke(cli, [\"-n\", \"--rebuild\", \"--no-build-isolation\"])\n    assert out.exit_code == 0\n    assert (\n        \"pip-tools[coverage,testing] @ git+https://github.com/jazzband/pip-tools@6.2.0\"\n        in out.stderr\n    )\n    assert \"pytest==\" in out.stderr\n    assert \"pytest-cov==\" in out.stderr\n\n\ndef test_input_file_without_extension(pip_conf, runner):\n    \"\"\"\n    piptools can compile a file without an extension,\n    and add .txt as the default output file extension.\n    \"\"\"\n    with open(\"requirements\", \"w\") as req_in:\n        req_in.write(\"small-fake-a==0.1\")\n\n    out = runner.invoke(cli, [\"requirements\"])\n\n    assert out.exit_code == 0\n    assert \"small-fake-a==0.1\" in out.stderr\n    assert os.path.exists(\"requirements.txt\")\n\n\ndef test_ignore_incompatible_existing_pins(pip_conf, runner):\n    \"\"\"\n    Successfully compile when existing output pins conflict with input.\n    \"\"\"\n    with open(\"requirements.txt\", \"w\") as req_txt:\n        req_txt.write(\"small-fake-a==0.2\\nsmall-fake-b==0.2\")\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"small-fake-with-deps\\nsmall-fake-b<0.2\")\n\n    out = runner.invoke(cli, [])\n\n    assert out.exit_code == 0\n\n\ndef test_upgrade_packages_option(pip_conf, runner):\n    \"\"\"\n    piptools respects --upgrade-package/-P inline list.\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"small-fake-a\\nsmall-fake-b\")\n    with open(\"requirements.txt\", \"w\") as req_in:\n        req_in.write(\"small-fake-a==0.1\\nsmall-fake-b==0.1\")\n\n    out = runner.invoke(cli, [\"--no-annotate\", \"-P\", \"small-fake-b\"])\n\n    assert out.exit_code == 0\n    assert \"small-fake-a==0.1\" in out.stderr.splitlines()\n    assert \"small-fake-b==0.3\" in out.stderr.splitlines()\n\n\ndef test_upgrade_packages_option_irrelevant(pip_conf, runner):\n    \"\"\"\n    piptools ignores --upgrade-package/-P items not already constrained.\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"small-fake-a\")\n    with open(\"requirements.txt\", \"w\") as req_in:\n        req_in.write(\"small-fake-a==0.1\")\n\n    out = runner.invoke(cli, [\"--no-annotate\", \"--upgrade-package\", \"small-fake-b\"])\n\n    assert out.exit_code == 0\n    assert \"small-fake-a==0.1\" in out.stderr.splitlines()\n    assert \"small-fake-b==0.3\" not in out.stderr.splitlines()\n\n\ndef test_upgrade_packages_option_no_existing_file(pip_conf, runner):\n    \"\"\"\n    piptools respects --upgrade-package/-P inline list when the output file\n    doesn't exist.\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"small-fake-a\\nsmall-fake-b\")\n\n    out = runner.invoke(cli, [\"--no-annotate\", \"-P\", \"small-fake-b\"])\n\n    assert out.exit_code == 0\n    assert \"small-fake-a==0.2\" in out.stderr.splitlines()\n    assert \"small-fake-b==0.3\" in out.stderr.splitlines()\n    assert (\n        \"WARNING: the output file requirements.txt exists but is empty\"\n        not in out.stderr\n    )\n\n\ndef test_upgrade_packages_empty_target_file_warning(pip_conf, runner):\n    \"\"\"\n    piptools warns the user if --upgrade-package/-P is specified and the\n    output file exists, but is empty.\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"small-fake-a==0.2\")\n    with open(\"requirements.txt\", \"w\") as req_txt:\n        req_txt.write(\"\")\n\n    out = runner.invoke(cli, [\"--no-annotate\", \"-P\", \"small-fake-a\"])\n\n    assert out.exit_code == 0\n    assert \"small-fake-a==0.2\" in out.stderr.splitlines()\n    assert \"WARNING: the output file requirements.txt exists but is empty\" in out.stderr\n\n\n@pytest.mark.parametrize(\n    (\"current_package\", \"upgraded_package\"),\n    (\n        pytest.param(\"small-fake-b==0.1\", \"small-fake-b==0.3\", id=\"upgrade\"),\n        pytest.param(\"small-fake-b==0.3\", \"small-fake-b==0.1\", id=\"downgrade\"),\n    ),\n)\ndef test_upgrade_packages_version_option(\n    pip_conf, runner, current_package, upgraded_package\n):\n    \"\"\"\n    piptools respects --upgrade-package/-P inline list with specified versions.\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"small-fake-a\\nsmall-fake-b\")\n    with open(\"requirements.txt\", \"w\") as req_in:\n        req_in.write(\"small-fake-a==0.1\\n\" + current_package)\n\n    out = runner.invoke(cli, [\"--no-annotate\", \"--upgrade-package\", upgraded_package])\n\n    assert out.exit_code == 0\n    stderr_lines = out.stderr.splitlines()\n    assert \"small-fake-a==0.1\" in stderr_lines\n    assert upgraded_package in stderr_lines\n\n\ndef test_upgrade_packages_version_option_no_existing_file(pip_conf, runner):\n    \"\"\"\n    piptools respects --upgrade-package/-P inline list with specified versions.\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"small-fake-a\\nsmall-fake-b\")\n\n    out = runner.invoke(cli, [\"-P\", \"small-fake-b==0.2\"])\n\n    assert out.exit_code == 0\n    assert \"small-fake-a==0.2\" in out.stderr\n    assert \"small-fake-b==0.2\" in out.stderr\n\n\n@pytest.mark.parametrize(\n    \"reqs_in\",\n    (\n        pytest.param(\"small-fake-a\\nsmall-fake-b\", id=\"direct reqs\"),\n        pytest.param(\"small-fake-with-unpinned-deps\", id=\"parent req\"),\n    ),\n)\ndef test_upgrade_packages_version_option_and_upgrade(pip_conf, runner, reqs_in):\n    \"\"\"\n    piptools respects --upgrade-package/-P inline list with specified versions\n    whilst also doing --upgrade.\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(reqs_in)\n    with open(\"requirements.txt\", \"w\") as req_in:\n        req_in.write(\"small-fake-a==0.1\\nsmall-fake-b==0.1\")\n\n    out = runner.invoke(cli, [\"--upgrade\", \"-P\", \"small-fake-b==0.1\"])\n\n    assert out.exit_code == 0\n    assert \"small-fake-a==0.2\" in out.stderr\n    assert \"small-fake-b==0.1\" in out.stderr\n\n\ndef test_upgrade_packages_version_option_and_upgrade_no_existing_file(pip_conf, runner):\n    \"\"\"\n    piptools respects --upgrade-package/-P inline list with specified versions\n    whilst also doing --upgrade and the output file doesn't exist.\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"small-fake-a\\nsmall-fake-b\")\n\n    out = runner.invoke(cli, [\"--upgrade\", \"-P\", \"small-fake-b==0.1\"])\n\n    assert out.exit_code == 0\n    assert \"small-fake-a==0.2\" in out.stderr\n    assert \"small-fake-b==0.1\" in out.stderr\n\n\ndef test_upgrade_package_with_extra(runner, make_package, make_sdist, tmpdir):\n    \"\"\"\n    piptools ignores extras on --upgrade-package/-P items if already constrained.\n    \"\"\"\n    test_package_1 = make_package(\n        \"test_package_1\", version=\"0.1\", extras_require={\"more\": \"test_package_2\"}\n    )\n    test_package_2 = make_package(\n        \"test_package_2\",\n        version=\"0.1\",\n    )\n    dists_dir = tmpdir / \"dists\"\n    for pkg in (test_package_1, test_package_2):\n        make_sdist(pkg, dists_dir)\n\n    # Constrain our requirement with an extra\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"test-package-1[more]\")\n\n    # Run update on test-package-1[more] -- this should be equivalent\n    # to running an update on test-package-1\n    out = runner.invoke(\n        cli,\n        [\n            \"--output-file\",\n            \"-\",\n            \"--quiet\",\n            \"--find-links\",\n            str(dists_dir),\n            \"--no-annotate\",\n            \"--no-header\",\n            \"--no-emit-options\",\n            \"--no-build-isolation\",\n            \"--upgrade-package\",\n            \"test-package-1[more]\",\n        ],\n    )\n\n    assert out.exit_code == 0, out\n    assert dedent(\"\"\"\\\n            test-package-1[more]==0.1\n            test-package-2==0.1\n            \"\"\") == out.stdout\n\n\ndef test_quiet_option(pip_conf, runner):\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"small-fake-a\")\n\n    out = runner.invoke(cli, [\"--quiet\"])\n    # Pinned requirements result has not been written to stderr:\n    assert b\"small-fake-a\" not in out.stderr_bytes\n\n\ndef test_dry_run_noisy_option(runner):\n    with open(\"requirements.in\", \"w\"):\n        pass\n    out = runner.invoke(cli, [\"--dry-run\"])\n    # Dry-run message has been written to output\n    assert \"Dry-run, so nothing updated.\" in out.stderr.splitlines()\n\n\ndef test_dry_run_quiet_option(runner):\n    with open(\"requirements.in\", \"w\"):\n        pass\n    out = runner.invoke(cli, [\"--output-file\", \"-\", \"--dry-run\", \"--quiet\"])\n    # Neither dry-run message nor pinned requirements written to output:\n    assert not out.stdout_bytes\n    # Dry-run message has not been written to stderr:\n    assert \"dry-run\" not in out.stderr.lower()\n    # Pinned requirements (just the header in this case) *are* written to stderr:\n    assert \"# \" in out.stderr\n\n\ndef test_generate_hashes_with_editable(pip_conf, runner):\n    small_fake_package_dir = os.path.join(PACKAGES_PATH, \"small_fake_with_deps\")\n    small_fake_package_url = path_to_url(small_fake_package_dir)\n    with open(\"requirements.in\", \"w\") as fp:\n        fp.write(f\"-e {small_fake_package_url}\\n\")\n    out = runner.invoke(cli, [\"--no-annotate\", \"--generate-hashes\"])\n    expected = (\n        \"-e {}\\n\"\n        \"small-fake-a==0.1 \\\\\\n\"\n        \"    --hash=sha256:5e6071ee6e4c59e0d0408d366f\"\n        \"e9b66781d2cf01be9a6e19a2433bb3c5336330\\n\"\n        \"small-fake-b==0.1 \\\\\\n\"\n        \"    --hash=sha256:acdba8f8b8a816213c30d5310c\"\n        \"3fe296c0107b16ed452062f7f994a5672e3b3f\\n\"\n    ).format(small_fake_package_url)\n    assert out.exit_code == 0\n    assert expected in out.stderr\n\n\n@pytest.mark.network\ndef test_generate_hashes_with_url(runner):\n    with open(\"requirements.in\", \"w\") as fp:\n        fp.write(\n            \"https://github.com/jazzband/pip-tools/archive/\"\n            \"7d86c8d3ecd1faa6be11c7ddc6b29a30ffd1dae3.zip#egg=pip-tools\\n\"\n        )\n    out = runner.invoke(cli, [\"--no-annotate\", \"--generate-hashes\"])\n    expected = (\n        \"pip-tools @ https://github.com/jazzband/pip-tools/archive/\"\n        \"7d86c8d3ecd1faa6be11c7ddc6b29a30ffd1dae3.zip \\\\\\n\"\n        \"    --hash=sha256:d24de92e18ad5bf291f25cfcdcf\"\n        \"0171be6fa70d01d0bef9eeda356b8549715e7\\n\"\n    )\n    assert out.exit_code == 0\n    assert expected in out.stderr\n\n\ndef test_generate_hashes_verbose(pip_conf, runner):\n    \"\"\"\n    The hashes generation process should show a progress.\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as fp:\n        fp.write(\"small-fake-a==0.1\")\n\n    out = runner.invoke(cli, [\"--generate-hashes\", \"-v\"])\n    expected_verbose_text = \"Generating hashes:\\n  small-fake-a\\n\"\n    assert expected_verbose_text in out.stderr\n\n\n@pytest.mark.network\ndef test_generate_hashes_with_annotations(runner):\n    with open(\"requirements.in\", \"w\") as fp:\n        fp.write(\"six==1.15.0\")\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--output-file\",\n            \"-\",\n            \"--quiet\",\n            \"--no-header\",\n            \"--generate-hashes\",\n        ],\n    )\n    assert out.stdout == dedent(\"\"\"\\\n        six==1.15.0 \\\\\n            --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \\\\\n            --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced\n            # via -r requirements.in\n        \"\"\")\n\n\n@pytest.mark.network\n@pytest.mark.parametrize(\"gen_hashes\", (True, False))\n@pytest.mark.parametrize(\n    \"annotate_options\",\n    (\n        (\"--no-annotate\",),\n        (\"--annotation-style\", \"line\"),\n        (\"--annotation-style\", \"split\"),\n    ),\n)\n@pytest.mark.parametrize(\n    (\"nl_options\", \"must_include\", \"must_exclude\"),\n    (\n        pytest.param((\"--newline\", \"lf\"), b\"\\n\", b\"\\r\\n\", id=\"LF\"),\n        pytest.param((\"--newline\", \"crlf\"), b\"\\r\\n\", b\"\\n\", id=\"CRLF\"),\n        pytest.param(\n            (\"--newline\", \"native\"),\n            os.linesep.encode(),\n            {\"\\n\": b\"\\r\\n\", \"\\r\\n\": b\"\\n\"}[os.linesep],\n            id=\"native\",\n        ),\n    ),\n)\ndef test_override_newline(\n    pip_conf,\n    runner,\n    gen_hashes,\n    annotate_options,\n    nl_options,\n    must_include,\n    must_exclude,\n    tmp_path,\n):\n    opts = annotate_options + nl_options\n    if gen_hashes:\n        opts += (\"--generate-hashes\",)\n\n    example_dir = tmp_path / \"example_dir\"\n    example_dir.mkdir()\n    in_path = example_dir / \"requirements.in\"\n    out_path = example_dir / \"requirements.txt\"\n    in_path.write_bytes(b\"small-fake-a==0.1\\nsmall-fake-b\\n\")\n\n    runner.invoke(\n        cli, [*opts, f\"--output-file={os.fsdecode(out_path)}\", os.fsdecode(in_path)]\n    )\n    txt = out_path.read_bytes()\n\n    assert must_include in txt\n\n    if must_exclude in must_include:\n        txt = txt.replace(must_include, b\"\")\n    assert must_exclude not in txt\n\n    # Do it again, with --newline=preserve:\n\n    opts = annotate_options + (\"--newline\", \"preserve\")\n    if gen_hashes:\n        opts += (\"--generate-hashes\",)\n\n    runner.invoke(\n        cli, [*opts, f\"--output-file={os.fsdecode(out_path)}\", os.fsdecode(in_path)]\n    )\n    txt = out_path.read_bytes()\n\n    assert must_include in txt\n\n    if must_exclude in must_include:\n        txt = txt.replace(must_include, b\"\")\n    assert must_exclude not in txt\n\n\n@pytest.mark.network\n@pytest.mark.parametrize(\n    (\"linesep\", \"must_exclude\"),\n    (pytest.param(\"\\n\", \"\\r\\n\", id=\"LF\"), pytest.param(\"\\r\\n\", \"\\n\", id=\"CRLF\")),\n)\ndef test_preserve_newline_from_input(runner, linesep, must_exclude):\n    with open(\"requirements.in\", \"wb\") as req_in:\n        req_in.write(f\"six{linesep}\".encode())\n\n    runner.invoke(cli, [\"--newline=preserve\", \"requirements.in\"])\n    with open(\"requirements.txt\", \"rb\") as req_txt:\n        txt = req_txt.read().decode()\n\n    assert linesep in txt\n\n    if must_exclude in linesep:\n        txt = txt.replace(linesep, \"\")\n    assert must_exclude not in txt\n\n\ndef test_generate_hashes_with_split_style_annotations(pip_conf, runner, tmpdir_cwd):\n    reqs_in = tmpdir_cwd / \"requirements.in\"\n    reqs_in.write_text(dedent(\"\"\"\\\n            small_fake_with_deps\n            small-fake-a\n            \"\"\"))\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--output-file\",\n            \"-\",\n            \"--quiet\",\n            \"--no-header\",\n            \"--no-emit-options\",\n            \"--generate-hashes\",\n            \"--annotation-style\",\n            \"split\",\n        ],\n    )\n\n    assert out.stdout == dedent(\"\"\"\\\n        small-fake-a==0.1 \\\\\n            --hash=sha256:5e6071ee6e4c59e0d0408d366fe9b66781d2cf01be9a6e19a2433bb3c5336330\n            # via\n            #   -r requirements.in\n            #   small-fake-with-deps\n        small-fake-with-deps==0.1 \\\\\n            --hash=sha256:71403033c0545516cc5066c9196d9490affae65a865af3198438be6923e4762e\n            # via -r requirements.in\n        \"\"\")\n\n\ndef test_generate_hashes_with_line_style_annotations(pip_conf, runner, tmpdir_cwd):\n    reqs_in = tmpdir_cwd / \"requirements.in\"\n    reqs_in.write_text(dedent(\"\"\"\\\n            small_fake_with_deps\n            small-fake-a\n            \"\"\"))\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--output-file\",\n            \"-\",\n            \"--quiet\",\n            \"--no-header\",\n            \"--no-emit-options\",\n            \"--generate-hashes\",\n            \"--annotation-style\",\n            \"line\",\n        ],\n    )\n\n    assert out.stdout == dedent(\"\"\"\\\n        small-fake-a==0.1 \\\\\n            --hash=sha256:5e6071ee6e4c59e0d0408d366fe9b66781d2cf01be9a6e19a2433bb3c5336330\n            # via -r requirements.in, small-fake-with-deps\n        small-fake-with-deps==0.1 \\\\\n            --hash=sha256:71403033c0545516cc5066c9196d9490affae65a865af3198438be6923e4762e\n            # via -r requirements.in\n        \"\"\")\n\n\n@pytest.mark.network\ndef test_generate_hashes_with_mixed_sources(\n    runner, make_package, make_wheel, make_sdist, tmp_path\n):\n    \"\"\"\n    Test that pip-compile generate hashes for every file from all given sources:\n    PyPI and/or --find-links.\n    \"\"\"\n\n    wheels_dir = tmp_path / \"wheels\"\n    wheels_dir.mkdir()\n\n    dummy_six_pkg = make_package(name=\"six\", version=\"1.16.0\")\n    make_wheel(dummy_six_pkg, wheels_dir, \"--build-number\", \"123\")\n\n    fav_hasher = hashlib.new(FAVORITE_HASH)\n    fav_hasher.update((wheels_dir / \"six-1.16.0-123-py3-none-any.whl\").read_bytes())\n    dummy_six_wheel_digest = fav_hasher.hexdigest()\n\n    with open(\"requirements.in\", \"w\") as fp:\n        fp.write(\"six==1.16.0\\n\")\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--output-file\",\n            \"-\",\n            \"--quiet\",\n            \"--no-header\",\n            \"--generate-hashes\",\n            \"--no-emit-options\",\n            \"--no-annotate\",\n            \"--find-links\",\n            wheels_dir.as_uri(),\n        ],\n    )\n\n    expected_digests = sorted(\n        (\n            # sdist hash for six-1.16.0.tar.gz from PyPI\n            \"1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926\",\n            # wheel hash for six-1.16.0-py2.py3-none-any.whl from PyPI\n            \"8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254\",\n            # wheel hash for local six-1.16.0-123-py3-none-any.whl file\n            dummy_six_wheel_digest,\n        )\n    )\n    expected_output = dedent(f\"\"\"\\\n        six==1.16.0 \\\\\n            --hash=sha256:{expected_digests[0]} \\\\\n            --hash=sha256:{expected_digests[1]} \\\\\n            --hash=sha256:{expected_digests[2]}\n        \"\"\")\n    assert out.stdout == expected_output\n\n\ndef test_filter_pip_markers(pip_conf, runner):\n    \"\"\"\n    Check that pip-compile works with pip environment markers (PEP496)\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"small-fake-a==0.1\\nunknown_package==0.1; python_version == '1'\")\n\n    out = runner.invoke(cli, [\"--output-file\", \"-\", \"--quiet\"])\n\n    assert out.exit_code == 0\n    assert \"small-fake-a==0.1\" in out.stdout\n    assert \"unknown_package\" not in out.stdout\n\n\ndef test_bad_setup_file(runner):\n    with open(\"setup.py\", \"w\") as package:\n        package.write(\"BAD SYNTAX\")\n\n    out = runner.invoke(cli, [\"--no-build-isolation\"])\n\n    assert out.exit_code == 2\n    assert f\"Failed to parse {os.path.abspath('setup.py')}\" in out.stderr\n\n\n@legacy_resolver_only\ndef test_no_candidates(pip_conf, runner):\n    with open(\"requirements\", \"w\") as req_in:\n        req_in.write(\"small-fake-a>0.3b1,<0.3b2\")\n\n    out = runner.invoke(cli, [\"-n\", \"requirements\"])\n\n    assert out.exit_code == 2\n    assert \"Skipped pre-versions:\" in out.stderr\n\n\n@legacy_resolver_only\ndef test_no_candidates_pre(pip_conf, runner):\n    with open(\"requirements\", \"w\") as req_in:\n        req_in.write(\"small-fake-a>0.3b1,<0.3b1\")\n\n    out = runner.invoke(cli, [\"-n\", \"requirements\", \"--pre\"])\n\n    assert out.exit_code == 2\n    assert \"Tried pre-versions:\" in out.stderr\n\n\n@pytest.mark.parametrize(\n    (\"url\", \"expected_url\"),\n    (\n        pytest.param(\"https://example.com\", b\"https://example.com\", id=\"regular url\"),\n        pytest.param(\n            \"https://username:password@example.com\",\n            b\"https://username:****@example.com\",\n            id=\"url with credentials\",\n        ),\n    ),\n)\ndef test_default_index_url(make_pip_conf, url, expected_url):\n    \"\"\"\n    Test help's output with default index URL.\n    \"\"\"\n    make_pip_conf(dedent(f\"\"\"\\\n            [global]\n            index-url = {url}\n            \"\"\"))\n\n    result = subprocess.run(\n        [sys.executable, \"-m\", \"piptools\", \"compile\", \"--help\"],\n        stdout=subprocess.PIPE,\n        check=True,\n    )\n\n    assert expected_url in result.stdout\n\n\ndef test_stdin_without_output_file(runner):\n    \"\"\"\n    The --output-file option is required for STDIN.\n    \"\"\"\n    out = runner.invoke(cli, [\"-n\", \"-\"])\n\n    assert out.exit_code == 2\n    assert \"--output-file is required if input is from stdin\" in out.stderr\n\n\ndef test_stdin(pip_conf, runner):\n    \"\"\"\n    Test compile requirements from STDIN.\n    \"\"\"\n    out = runner.invoke(\n        cli,\n        [\"-\", \"--output-file\", \"-\", \"--quiet\", \"--no-emit-options\", \"--no-header\"],\n        input=\"small-fake-a==0.1\",\n    )\n\n    assert out.stdout == dedent(\"\"\"\\\n        small-fake-a==0.1\n            # via -r -\n        \"\"\")\n\n\ndef test_tmpfile_for_stdin_is_cleaned_up(pip_conf, runner):\n    \"\"\"\n    Test that when compiling requirements from stdin, a tempfile gets written with\n    those requirements and cleaned up.\n    \"\"\"\n    tmpfile_name = None\n\n    # save the real constructor so that it can be used while `mock.patch()` is\n    # active\n    real_constructor = tempfile_compat.named_temp_file\n\n    # a \"spy\" which can be mocked into place for `named_temp_file` to\n    # replace the implementation with one which has side-effects and makes test\n    # assertions\n    #\n    # this spy ensures that the file is deleted on exit\n    class NamedTempfileSpy:\n        def __init__(self, *args, **kwargs):\n            self._ctx_manager = real_constructor(*args, **kwargs)\n\n        def __enter__(self):\n            ret = self._ctx_manager.__enter__()\n            nonlocal tmpfile_name\n            tmpfile_name = ret.name\n            return ret\n\n        def __exit__(self, *args, **kwargs):\n            assert os.path.exists(tmpfile_name)\n            ret = self._ctx_manager.__exit__(*args, **kwargs)\n            assert not os.path.exists(tmpfile_name)\n            return ret\n\n    with mock.patch(\n        \"piptools._compat.tempfile_compat.named_temp_file\", NamedTempfileSpy\n    ):\n        out = runner.invoke(\n            cli,\n            [\"-\", \"--output-file\", \"-\", \"--quiet\", \"--no-emit-options\", \"--no-header\"],\n            input=\"small-fake-a==0.1\",\n        )\n\n    assert tmpfile_name is not None  # ensure the spy was used\n    assert out.stdout == dedent(\"\"\"\\\n        small-fake-a==0.1\n            # via -r -\n        \"\"\")\n\n\ndef test_multiple_input_files_without_output_file(runner):\n    \"\"\"\n    The --output-file option is required for multiple requirement input files.\n    \"\"\"\n    with open(\"src_file1.in\", \"w\") as req_in:\n        req_in.write(\"six==1.10.0\")\n\n    with open(\"src_file2.in\", \"w\") as req_in:\n        req_in.write(\"django==2.1\")\n\n    out = runner.invoke(cli, [\"src_file1.in\", \"src_file2.in\"])\n\n    assert (\n        \"--output-file is required if two or more input files are given\" in out.stderr\n    )\n    assert out.exit_code == 2\n\n\n@pytest.mark.parametrize(\n    (\"options\", \"expected\"),\n    (\n        pytest.param(\n            (\"--annotate\",),\n            \"\"\"\\\n            small-fake-a==0.1\n                # via\n                #   -c constraints.txt\n                #   small-fake-with-deps\n            small-fake-with-deps==0.1\n                # via -r requirements.in\n            \"\"\",\n            id=\"annotate\",\n        ),\n        pytest.param(\n            (\"--annotate\", \"--annotation-style\", \"line\"),\n            \"\"\"\\\n            small-fake-a==0.1         # via -c constraints.txt, small-fake-with-deps\n            small-fake-with-deps==0.1  # via -r requirements.in\n            \"\"\",\n            id=\"annotate line style\",\n        ),\n        pytest.param(\n            (\"--no-annotate\",),\n            \"\"\"\\\n            small-fake-a==0.1\n            small-fake-with-deps==0.1\n            \"\"\",\n            id=\"no annotate\",\n        ),\n    ),\n)\ndef test_annotate_option(pip_conf, runner, options, expected):\n    \"\"\"\n    The output lines have annotations if the option is turned on.\n    \"\"\"\n    with open(\"constraints.txt\", \"w\") as constraints_in:\n        constraints_in.write(\"small-fake-a==0.1\")\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"-c constraints.txt\\n\")\n        req_in.write(\"small_fake_with_deps\")\n\n    out = runner.invoke(\n        cli,\n        [*options, \"--output-file\", \"-\", \"--quiet\", \"--no-emit-options\", \"--no-header\"],\n    )\n\n    assert out.exit_code == 0, out\n    assert out.stdout == dedent(expected)\n\n\n@pytest.mark.parametrize(\n    (\"option\", \"expected\"),\n    (\n        pytest.param(\n            \"--allow-unsafe\",\n            dedent(\"\"\"\\\n                small-fake-a==0.1\n                small-fake-b==0.3\n\n                # The following packages are considered to be unsafe in a requirements file:\n                small-fake-with-deps==0.1\n                \"\"\"),\n            id=\"allow all packages\",\n        ),\n        pytest.param(\n            \"--no-allow-unsafe\",\n            dedent(\"\"\"\\\n                small-fake-a==0.1\n                small-fake-b==0.3\n\n                # The following packages are considered to be unsafe in a requirements file:\n                # small-fake-with-deps\n                \"\"\"),\n            id=\"comment out small-fake-with-deps and its dependencies\",\n        ),\n        pytest.param(\n            None,\n            dedent(\"\"\"\\\n                small-fake-a==0.1\n                small-fake-b==0.3\n\n                # The following packages are considered to be unsafe in a requirements file:\n                # small-fake-with-deps\n                \"\"\"),\n            id=\"allow unsafe is default option\",\n        ),\n    ),\n)\ndef test_allow_unsafe_option(pip_conf, monkeypatch, runner, option, expected):\n    \"\"\"\n    Unsafe packages are printed as expected with and without --allow-unsafe.\n    \"\"\"\n    monkeypatch.setattr(\"piptools.resolver.UNSAFE_PACKAGES\", {\"small-fake-with-deps\"})\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"small-fake-b\\n\")\n        req_in.write(\"small-fake-with-deps\")\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--output-file\",\n            \"-\",\n            \"--quiet\",\n            \"--no-header\",\n            \"--no-emit-options\",\n            \"--no-annotate\",\n            *([option] if option else []),\n        ],\n    )\n\n    assert out.exit_code == 0, out\n    assert out.stdout == expected\n\n\n@pytest.mark.parametrize(\n    (\"unsafe_package\", \"expected\"),\n    (\n        (\n            \"small-fake-with-deps\",\n            dedent(\"\"\"\\\n                small-fake-a==0.1\n                small-fake-b==0.3\n\n                # The following packages are considered to be unsafe in a requirements file:\n                # small-fake-with-deps\n                \"\"\"),\n        ),\n        (\n            \"small-fake-a\",\n            dedent(\"\"\"\\\n                small-fake-b==0.3\n                small-fake-with-deps==0.1\n\n                # The following packages are considered to be unsafe in a requirements file:\n                # small-fake-a\n                \"\"\"),\n        ),\n    ),\n)\ndef test_unsafe_package_option(pip_conf, monkeypatch, runner, unsafe_package, expected):\n    monkeypatch.setattr(\"piptools.resolver.UNSAFE_PACKAGES\", {\"small-fake-with-deps\"})\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"small-fake-b\\n\")\n        req_in.write(\"small-fake-with-deps\")\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--output-file\",\n            \"-\",\n            \"--quiet\",\n            \"--no-header\",\n            \"--no-emit-options\",\n            \"--no-annotate\",\n            \"--unsafe-package\",\n            unsafe_package,\n        ],\n    )\n\n    assert out.exit_code == 0, out\n    assert out.stdout == expected\n\n\n@pytest.mark.parametrize(\n    \"unsafe_package\",\n    (\n        pytest.param(\"small-fake-with-deps\", id=\"kebab-case\"),\n        pytest.param(\"small_fake_with_deps\", id=\"snake_case\"),\n        pytest.param(\"Small_Fake_With_Deps\", id=\"mixed-case\"),\n    ),\n)\ndef test_unsafe_package_option_normalizes(pip_conf, runner, unsafe_package):\n    \"\"\"\n    The --unsafe-package option should normalize package names.\n    \"\"\"\n    pathlib.Path(\"requirements.in\").write_text(\n        dedent(\"\"\"\\\n        small_fake_b\n        small-fake-with-deps\n        \"\"\"),\n        encoding=\"utf-8\",\n    )\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--output-file\",\n            \"-\",\n            \"--quiet\",\n            \"--no-header\",\n            \"--no-emit-options\",\n            \"--no-annotate\",\n            \"--no-allow-unsafe\",\n            \"--unsafe-package\",\n            unsafe_package,\n        ],\n    )\n\n    assert out.exit_code == 0, out\n    assert out.stdout == dedent(\"\"\"\\\n            small-fake-a==0.1\n            small-fake-b==0.3\n\n            # The following packages are considered to be unsafe in a requirements file:\n            # small-fake-with-deps\n            \"\"\")\n\n\n@pytest.mark.parametrize(\n    (\"option\", \"attr\", \"expected\"),\n    ((\"--cert\", \"cert\", \"foo.crt\"), (\"--client-cert\", \"client_cert\", \"bar.pem\")),\n)\n@mock.patch(\"piptools.scripts.compile.parse_requirements\")\ndef test_cert_option(parse_requirements, runner, option, attr, expected):\n    \"\"\"\n    The options --cert and --client-cert have to be passed to the PyPIRepository.\n    \"\"\"\n    with open(\"requirements.in\", \"w\"):\n        pass\n\n    runner.invoke(cli, [option, expected])\n\n    # Ensure the options in parse_requirements has the expected option\n    args, kwargs = parse_requirements.call_args\n    assert getattr(kwargs[\"options\"], attr) == expected\n\n\n@pytest.mark.parametrize(\n    (\"option\", \"expected\"),\n    ((\"--build-isolation\", True), (\"--no-build-isolation\", False)),\n)\n@mock.patch(\"piptools.scripts.compile.parse_requirements\")\ndef test_parse_requirements_build_isolation_option(\n    parse_requirements, runner, option, expected\n):\n    \"\"\"\n    A value of the --build-isolation/--no-build-isolation flag\n    must be passed to parse_requirements().\n    \"\"\"\n    with open(\"requirements.in\", \"w\"):\n        pass\n\n    runner.invoke(cli, [option])\n\n    # Ensure the options in parse_requirements has the expected build_isolation option\n    args, kwargs = parse_requirements.call_args\n    assert kwargs[\"options\"].build_isolation is expected\n\n\n@pytest.mark.parametrize(\n    (\"option\", \"expected\"),\n    ((\"--build-isolation\", True), (\"--no-build-isolation\", False)),\n)\n@mock.patch(\"piptools.scripts.compile.build_project_metadata\")\ndef test_build_project_metadata_isolation_option(\n    build_project_metadata, runner, option, expected\n):\n    \"\"\"\n    A value of the --build-isolation/--no-build-isolation flag\n    must be passed to build_project_metadata().\n    \"\"\"\n\n    with open(\"setup.py\", \"w\") as package:\n        package.write(dedent(\"\"\"\\\n                from setuptools import setup\n                setup(install_requires=[])\n                \"\"\"))\n\n    runner.invoke(cli, [option])\n\n    # Ensure the options in build_project_metadata has the isolated kwarg\n    _, kwargs = build_project_metadata.call_args\n    assert kwargs[\"isolated\"] is expected\n\n\n@mock.patch(\"piptools.scripts.compile.PyPIRepository\")\ndef test_forwarded_args(PyPIRepository, runner):\n    \"\"\"\n    Test the forwarded cli args (--pip-args 'arg...') are passed to the pip command.\n    \"\"\"\n    with open(\"requirements.in\", \"w\"):\n        pass\n\n    cli_args = (\"--no-annotate\", \"--generate-hashes\")\n    pip_args = (\"--no-color\", \"--isolated\", \"--disable-pip-version-check\")\n    runner.invoke(cli, [*cli_args, \"--pip-args\", \" \".join(pip_args)])\n    args, kwargs = PyPIRepository.call_args\n    assert set(pip_args).issubset(set(args[0]))\n\n\n@pytest.mark.parametrize(\n    \"pip_args\",\n    (\n        pytest.param(\n            (\"--use-pep517\", \"--global-option=build_ext\"),\n            id=\"use-pep517 and global-option\",\n        ),\n        pytest.param(\n            (\"--no-use-pep517\", \"--build-option=build_ext\"),\n            id=\"no-use-pep517 and build-option\",\n        ),\n    ),\n)\n@mock.patch(\"piptools.scripts.compile.PyPIRepository\")\ndef test_forwarded_args_filter_deprecated(PyPIRepository, runner, pip_args):\n    \"\"\"\n    Test the cli args (``--pip-args 'arg...'``) are filtered out if pip no longer supports them.\n    \"\"\"\n    pathlib.Path(\"requirements.in\").write_text(\"\", encoding=\"utf-8\")\n\n    cli_args = (\"--no-annotate\", \"--generate-hashes\")\n    runner.invoke(cli, [*cli_args, \"--pip-args\", shlex.join(pip_args)])\n    pip_option_keys = {pip_arg.split(\"=\")[0] for pip_arg in pip_args}\n\n    (first_posarg, *_tail_args), _kwargs = PyPIRepository.call_args\n\n    if _pip_api.PIP_VERSION_MAJOR_MINOR >= (25, 3):  # pragma: >=3.9 cover\n        assert set(first_posarg) ^ pip_option_keys\n    else:\n        assert set(first_posarg) & pip_option_keys\n\n\n@pytest.mark.parametrize(\n    (\"cli_option\", \"infile_option\", \"expected_package\"),\n    (\n        # no --pre pip-compile should resolve to the last stable version\n        (False, False, \"small-fake-a==0.2\"),\n        # pip-compile --pre should resolve to the last pre-released version\n        (True, False, \"small-fake-a==0.3b1\"),\n        (False, True, \"small-fake-a==0.3b1\"),\n        (True, True, \"small-fake-a==0.3b1\"),\n    ),\n)\ndef test_pre_option(pip_conf, runner, cli_option, infile_option, expected_package):\n    \"\"\"\n    Tests pip-compile respects --pre option.\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as req_in:\n        if infile_option:\n            req_in.write(\"--pre\\n\")\n        req_in.write(\"small-fake-a\\n\")\n\n    out = runner.invoke(\n        cli, [\"--no-annotate\", \"-n\"] + ([\"--pre\"] if cli_option else [])\n    )\n\n    assert out.exit_code == 0, out.stderr\n    assert expected_package in out.stderr.splitlines(), out.stderr\n\n\n@pytest.mark.parametrize(\n    \"add_options\",\n    (\n        [],\n        [\"--output-file\", \"requirements.txt\"],\n        [\"--upgrade\"],\n        [\"--upgrade\", \"--output-file\", \"requirements.txt\"],\n        [\"--upgrade-package\", \"small-fake-a\"],\n        [\"--upgrade-package\", \"small-fake-a\", \"--output-file\", \"requirements.txt\"],\n    ),\n)\ndef test_dry_run_option(pip_conf, runner, add_options):\n    \"\"\"\n    Tests pip-compile doesn't create requirements.txt file on dry-run.\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"small-fake-a\\n\")\n\n    out = runner.invoke(cli, [\"--no-annotate\", \"--dry-run\", *add_options])\n\n    assert out.exit_code == 0, out.stderr\n    assert \"small-fake-a==0.2\" in out.stderr.splitlines()\n    assert not os.path.exists(\"requirements.txt\")\n\n\n@pytest.mark.parametrize(\n    (\"add_options\", \"expected_cli_output_package\"),\n    (\n        ([], \"small-fake-a==0.1\"),\n        ([\"--output-file\", \"requirements.txt\"], \"small-fake-a==0.1\"),\n        ([\"--upgrade\"], \"small-fake-a==0.2\"),\n        ([\"--upgrade\", \"--output-file\", \"requirements.txt\"], \"small-fake-a==0.2\"),\n        ([\"--upgrade-package\", \"small-fake-a\"], \"small-fake-a==0.2\"),\n        (\n            [\"--upgrade-package\", \"small-fake-a\", \"--output-file\", \"requirements.txt\"],\n            \"small-fake-a==0.2\",\n        ),\n    ),\n)\ndef test_dry_run_doesnt_touch_output_file(\n    pip_conf, runner, add_options, expected_cli_output_package\n):\n    \"\"\"\n    Tests pip-compile doesn't touch requirements.txt file on dry-run.\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"small-fake-a\\n\")\n\n    with open(\"requirements.txt\", \"w\") as req_txt:\n        req_txt.write(\"small-fake-a==0.1\\n\")\n\n    before_compile_mtime = os.stat(\"requirements.txt\").st_mtime\n\n    out = runner.invoke(cli, [\"--no-annotate\", \"--dry-run\", *add_options])\n\n    assert out.exit_code == 0, out.stderr\n    assert expected_cli_output_package in out.stderr.splitlines()\n\n    # The package version must NOT be updated in the output file\n    with open(\"requirements.txt\") as req_txt:\n        assert \"small-fake-a==0.1\" in req_txt.read().splitlines()\n\n    # The output file must not be touched\n    after_compile_mtime = os.stat(\"requirements.txt\").st_mtime\n    assert after_compile_mtime == before_compile_mtime\n\n\n@pytest.mark.parametrize(\n    (\"empty_input_pkg\", \"prior_output_pkg\"),\n    (\n        (\"\", \"\"),\n        (\"\", \"small-fake-a==0.1\\n\"),\n        (\"# Nothing to see here\", \"\"),\n        (\"# Nothing to see here\", \"small-fake-a==0.1\\n\"),\n    ),\n)\ndef test_empty_input_file_no_header(runner, empty_input_pkg, prior_output_pkg):\n    \"\"\"\n    Tests pip-compile creates an empty requirements.txt file,\n    given --no-header and empty requirements.in\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(empty_input_pkg)  # empty input file\n\n    with open(\"requirements.txt\", \"w\") as req_txt:\n        req_txt.write(prior_output_pkg)\n\n    runner.invoke(cli, [\"--no-header\", \"requirements.in\"])\n\n    with open(\"requirements.txt\") as req_txt:\n        assert req_txt.read().strip() == \"\"\n\n\ndef test_upgrade_package_doesnt_remove_annotation(pip_conf, runner):\n    \"\"\"\n    Tests pip-compile --upgrade-package shouldn't remove \"via\" annotation.\n    See: GH-929\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"small-fake-with-deps\\n\")\n\n    runner.invoke(cli)\n\n    # Downgrade small-fake-a to 0.1\n    with open(\"requirements.txt\", \"w\") as req_txt:\n        req_txt.write(\n            \"small-fake-with-deps==0.1\\n\"\n            \"small-fake-a==0.1         # via small-fake-with-deps\\n\"\n        )\n\n    runner.invoke(cli, [\"-P\", \"small-fake-a\", \"--no-emit-options\", \"--no-header\"])\n    with open(\"requirements.txt\") as req_txt:\n        assert req_txt.read() == dedent(\"\"\"\\\n            small-fake-a==0.1\n                # via small-fake-with-deps\n            small-fake-with-deps==0.1\n                # via -r requirements.in\n            \"\"\")\n\n\n@pytest.mark.parametrize((\"num_inputs\"), (2, 3, 10))\ndef test_many_inputs_includes_all_annotations(pip_conf, runner, tmp_path, num_inputs):\n    \"\"\"\n    Tests that an entry required by multiple input files is attributed to all of them in the\n    annotation.\n    See: https://github.com/jazzband/pip-tools/issues/1853\n    \"\"\"\n    req_ins = [tmp_path / f\"requirements{n:02d}.in\" for n in range(num_inputs)]\n    for req_in in req_ins:\n        req_in.write_text(\"small-fake-a==0.1\\n\")\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--output-file\",\n            \"-\",\n            \"--quiet\",\n            \"--no-header\",\n            \"--no-emit-find-links\",\n        ]\n        + [str(r) for r in req_ins],\n    )\n    assert out.exit_code == 0, out.stderr\n    assert (\n        out.stdout\n        == \"\\n\".join(\n            [\n                \"small-fake-a==0.1\",\n                \"    # via\",\n            ]\n            + [f\"    #   -r {req_in.as_posix()}\" for req_in in req_ins]\n        )\n        + \"\\n\"\n    )\n\n\n@pytest.mark.parametrize(\n    \"options\",\n    (\n        \"--index-url https://example.com\",\n        \"--extra-index-url https://example.com\",\n        \"--find-links ./libs1\",\n        \"--trusted-host example.com\",\n        \"--no-binary :all:\",\n        \"--only-binary :all:\",\n    ),\n)\ndef test_options_in_requirements_file(runner, options):\n    \"\"\"\n    Test the options from requirements.in is copied to requirements.txt.\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as reqs_in:\n        reqs_in.write(options)\n\n    out = runner.invoke(cli)\n    assert out.exit_code == 0, out\n\n    with open(\"requirements.txt\") as reqs_txt:\n        assert options in reqs_txt.read().splitlines()\n\n\n@pytest.mark.parametrize(\n    (\"cli_options\", \"expected_message\"),\n    (\n        pytest.param(\n            [\"--index-url\", \"scheme://foo\"],\n            \"Was scheme://foo reachable?\",\n            id=\"single index url\",\n        ),\n        pytest.param(\n            [\"--index-url\", \"scheme://foo\", \"--extra-index-url\", \"scheme://bar\"],\n            \"Were scheme://foo or scheme://bar reachable?\",\n            id=\"multiple index urls\",\n        ),\n        pytest.param(\n            [\"--index-url\", \"scheme://username:password@host\"],\n            \"Was scheme://username:****@host reachable?\",\n            id=\"index url with credentials\",\n        ),\n    ),\n)\n@legacy_resolver_only\ndef test_unreachable_index_urls(runner, cli_options, expected_message):\n    \"\"\"\n    Test pip-compile raises an error if index URLs are not reachable.\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as reqs_in:\n        reqs_in.write(\"some-package\")\n\n    out = runner.invoke(cli, cli_options)\n\n    assert out.exit_code == 2, out\n\n    stderr_lines = out.stderr.splitlines()\n    assert \"No versions found\" in stderr_lines\n    assert expected_message in stderr_lines\n\n\n@pytest.mark.parametrize(\"subdep_already_pinned\", (True, False))\n@pytest.mark.parametrize(\n    (\"current_package\", \"upgraded_package\"),\n    (\n        pytest.param(\"small-fake-b==0.1\", \"small-fake-b==0.2\", id=\"upgrade\"),\n        pytest.param(\"small-fake-b==0.2\", \"small-fake-b==0.1\", id=\"downgrade\"),\n    ),\n)\ndef test_upgrade_packages_option_subdependency(\n    pip_conf, runner, current_package, upgraded_package, subdep_already_pinned\n):\n    \"\"\"\n    Test that pip-compile --upgrade-package/-P upgrades/downgrades subdependencies.\n    \"\"\"\n\n    with open(\"requirements.in\", \"w\") as reqs:\n        reqs.write(\"small-fake-with-unpinned-deps\\n\")\n\n    with open(\"requirements.txt\", \"w\") as reqs:\n        reqs.write(\"small-fake-a==0.1\\n\")\n        if subdep_already_pinned:\n            reqs.write(current_package + \"\\n\")\n        reqs.write(\"small-fake-with-unpinned-deps==0.1\\n\")\n\n    out = runner.invoke(\n        cli, [\"--no-annotate\", \"--dry-run\", \"--upgrade-package\", upgraded_package]\n    )\n\n    stderr_lines = out.stderr.splitlines()\n    assert \"small-fake-a==0.1\" in stderr_lines, \"small-fake-a must keep its version\"\n    assert (\n        upgraded_package in stderr_lines\n    ), f\"{current_package} must be upgraded/downgraded to {upgraded_package}\"\n\n\n@pytest.mark.parametrize(\n    (\"input_opts\", \"output_opts\"),\n    (\n        # Test that input options overwrite output options\n        pytest.param(\n            \"--index-url https://index-url\",\n            \"--index-url https://another-index-url\",\n            id=\"index url\",\n        ),\n        pytest.param(\n            \"--extra-index-url https://extra-index-url\",\n            \"--extra-index-url https://another-extra-index-url\",\n            id=\"extra index url\",\n        ),\n        pytest.param(\"--find-links dir\", \"--find-links another-dir\", id=\"find links\"),\n        pytest.param(\n            \"--trusted-host hostname\",\n            \"--trusted-host another-hostname\",\n            id=\"trusted host\",\n        ),\n        pytest.param(\n            \"--no-binary :package:\", \"--no-binary :another-package:\", id=\"no binary\"\n        ),\n        pytest.param(\n            \"--only-binary :package:\",\n            \"--only-binary :another-package:\",\n            id=\"only binary\",\n        ),\n        # Test misc corner cases\n        pytest.param(\"\", \"--index-url https://index-url\", id=\"empty input options\"),\n        pytest.param(\n            \"--index-url https://index-url\",\n            (\n                \"--index-url https://index-url\\n\"\n                \"--extra-index-url https://another-extra-index-url\"\n            ),\n            id=\"partially matched options\",\n        ),\n    ),\n)\ndef test_remove_outdated_options(runner, input_opts, output_opts):\n    \"\"\"\n    Test that the options from the current requirements.txt wouldn't stay\n    after compile if they were removed from requirements.in file.\n    \"\"\"\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(input_opts)\n    with open(\"requirements.txt\", \"w\") as req_txt:\n        req_txt.write(output_opts)\n\n    out = runner.invoke(cli, [\"--output-file\", \"-\", \"--quiet\", \"--no-header\"])\n\n    assert out.exit_code == 0, out\n    assert out.stdout.strip() == input_opts\n\n\ndef test_sub_dependencies_with_constraints(pip_conf, runner):\n    # Write constraints file\n    with open(\"constraints.txt\", \"w\") as constraints_in:\n        constraints_in.write(\"small-fake-a==0.1\\n\")\n        constraints_in.write(\"small-fake-b==0.2\\n\")\n        constraints_in.write(\"small-fake-with-unpinned-deps==0.1\")\n\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"-c constraints.txt\\n\")\n        req_in.write(\"small_fake_with_deps_and_sub_deps\")  # require fake package\n\n    out = runner.invoke(cli, [\"--no-annotate\"])\n\n    assert out.exit_code == 0\n\n    req_out_lines = set(out.stderr.splitlines())\n    assert {\n        \"small-fake-a==0.1\",\n        \"small-fake-b==0.2\",\n        \"small-fake-with-deps-and-sub-deps==0.1\",\n        \"small-fake-with-unpinned-deps==0.1\",\n    }.issubset(req_out_lines)\n\n\ndef test_preserve_compiled_prerelease_version(pip_conf, runner):\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"small-fake-a\")\n\n    with open(\"requirements.txt\", \"w\") as req_txt:\n        req_txt.write(\"small-fake-a==0.3b1\")\n\n    out = runner.invoke(cli, [\"--no-annotate\", \"--no-header\"])\n\n    assert out.exit_code == 0, out\n    assert \"small-fake-a==0.3b1\" in out.stderr.splitlines()\n\n\n@backtracking_resolver_only\ndef test_ignore_compiled_unavailable_version(pip_conf, runner, current_resolver):\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"small-fake-a\")\n\n    with open(\"requirements.txt\", \"w\") as req_txt:\n        req_txt.write(\"small-fake-a==9999\")\n\n    out = runner.invoke(cli, [\"--no-annotate\", \"--no-header\"])\n\n    assert out.exit_code == 0, out\n    assert \"small-fake-a==\" in out.stderr\n    assert \"small-fake-a==9999\" not in out.stderr.splitlines()\n\n    assert (\n        \"Discarding small-fake-a==9999 \"\n        \"(from -r requirements.txt (line 1)) \"\n        \"to proceed the resolution\"\n    ) in out.stderr\n\n\ndef test_prefer_binary_dist(\n    pip_conf, make_package, make_sdist, make_wheel, tmpdir, runner\n):\n    \"\"\"\n    Test pip-compile chooses a correct version of a package with\n    a binary distribution when PIP_PREFER_BINARY environment variable is on.\n    \"\"\"\n    dists_dir = tmpdir / \"dists\"\n\n    # Make first-package==1.0 and wheels\n    first_package_v1 = make_package(name=\"first-package\", version=\"1.0\")\n    make_wheel(first_package_v1, dists_dir)\n\n    # Make first-package==2.0 and sdists\n    first_package_v2 = make_package(name=\"first-package\", version=\"2.0\")\n    make_sdist(first_package_v2, dists_dir)\n\n    # Make second-package==1.0 which depends on first-package, and wheels\n    second_package_v1 = make_package(\n        name=\"second-package\", version=\"1.0\", install_requires=[\"first-package\"]\n    )\n    make_wheel(second_package_v1, dists_dir)\n\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"second-package\")\n\n    out = runner.invoke(\n        cli,\n        [\"--no-annotate\", \"--find-links\", str(dists_dir)],\n        env={\"PIP_PREFER_BINARY\": \"1\"},\n    )\n\n    assert out.exit_code == 0, out\n    assert \"first-package==1.0\" in out.stderr.splitlines(), out.stderr\n    assert \"second-package==1.0\" in out.stderr.splitlines(), out.stderr\n\n\n@pytest.mark.parametrize(\"prefer_binary\", (True, False))\ndef test_prefer_binary_dist_even_there_is_source_dists(\n    pip_conf, make_package, make_sdist, make_wheel, tmpdir, runner, prefer_binary\n):\n    \"\"\"\n    Test pip-compile chooses a correct version of a package with a binary distribution\n    (despite a source dist existing) when PIP_PREFER_BINARY environment variable is on\n    or off.\n\n    Regression test for issue GH-1118.\n    \"\"\"\n    dists_dir = tmpdir / \"dists\"\n\n    # Make first version of package with only wheels\n    package_v1 = make_package(name=\"test-package\", version=\"1.0\")\n    make_wheel(package_v1, dists_dir)\n\n    # Make seconds version with wheels and sdists\n    package_v2 = make_package(name=\"test-package\", version=\"2.0\")\n    make_wheel(package_v2, dists_dir)\n    make_sdist(package_v2, dists_dir)\n\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"test-package\")\n\n    out = runner.invoke(\n        cli,\n        [\"--no-annotate\", \"--find-links\", str(dists_dir)],\n        env={\"PIP_PREFER_BINARY\": str(int(prefer_binary))},\n    )\n\n    assert out.exit_code == 0, out\n    assert \"test-package==2.0\" in out.stderr.splitlines(), out.stderr\n\n\n@pytest.mark.parametrize(\"output_content\", (\"test-package-1==0.1\", \"\"))\ndef test_duplicate_reqs_combined(\n    pip_conf, make_package, make_sdist, tmpdir, runner, output_content\n):\n    \"\"\"\n    Test pip-compile tracks dependencies properly when install requirements are\n    combined, especially when an output file already exists.\n\n    Regression test for issue GH-1154.\n    \"\"\"\n    test_package_1 = make_package(\"test_package_1\", version=\"0.1\")\n    test_package_2 = make_package(\n        \"test_package_2\", version=\"0.1\", install_requires=[\"test-package-1\"]\n    )\n\n    dists_dir = tmpdir / \"dists\"\n\n    for pkg in (test_package_1, test_package_2):\n        make_sdist(pkg, dists_dir)\n\n    with open(\"requirements.in\", \"w\") as reqs_in:\n        reqs_in.write(f\"file:{test_package_2}\\n\")\n        reqs_in.write(f\"file:{test_package_2}#egg=test-package-2\\n\")\n\n    if output_content:\n        with open(\"requirements.txt\", \"w\") as reqs_out:\n            reqs_out.write(output_content)\n\n    out = runner.invoke(cli, [\"--find-links\", str(dists_dir)])\n\n    assert out.exit_code == 0, out\n    assert str(test_package_2) in out.stderr\n    assert \"test-package-1==0.1\" in out.stderr\n\n\ndef test_local_duplicate_subdependency_combined(runner, make_package):\n    \"\"\"\n    Test pip-compile tracks subdependencies properly when install requirements\n    are combined, especially when local paths are passed as urls, and those reqs\n    are combined after getting dependencies.\n\n    Regression test for issue GH-1505.\n    \"\"\"\n    package_a = make_package(\"project-a\", install_requires=[\"pip-tools==6.3.0\"])\n    package_b = make_package(\"project-b\", install_requires=[\"project-a\"])\n\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.writelines(\n            [\n                f\"{path_to_url(str(package_a))}#egg=project-a\\n\",\n                f\"{path_to_url(str(package_b))}#egg=project-b\",\n            ]\n        )\n\n    out = runner.invoke(cli, [\"-n\"])\n\n    assert out.exit_code == 0\n    assert \"project-b\" in out.stderr\n    assert \"project-a\" in out.stderr\n    assert \"pip-tools==6.3.0\" in out.stderr\n    assert \"click\" in out.stderr  # dependency of pip-tools\n\n\ndef test_combine_extras(pip_conf, runner, make_package):\n    \"\"\"\n    Ensure that multiple declarations of a dependency that specify different\n    extras produces a requirement for that package with the union of the extras\n    \"\"\"\n    package_with_extras = make_package(\n        \"package_with_extras\",\n        extras_require={\n            \"extra1\": [\"small-fake-a==0.1\"],\n            \"extra2\": [\"small-fake-b==0.1\"],\n        },\n    )\n\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.writelines(\n            [\n                \"-r ./requirements-second.in\\n\",\n                f\"{package_with_extras}[extra1]\",\n            ]\n        )\n\n    with open(\"requirements-second.in\", \"w\") as req_sec_in:\n        req_sec_in.write(f\"{package_with_extras}[extra2]\")\n\n    out = runner.invoke(cli, [\"-n\"])\n\n    assert out.exit_code == 0\n    assert \"package-with-extras\" in out.stderr\n    assert \"small-fake-a==\" in out.stderr\n    assert \"small-fake-b==\" in out.stderr\n\n\ndef test_combine_different_extras_of_the_same_package(\n    pip_conf, runner, tmpdir, make_package, make_wheel\n):\n    \"\"\"\n    Loosely based on the example from https://github.com/jazzband/pip-tools/issues/1511.\n    \"\"\"\n    pkgs = [\n        make_package(\n            \"fake-colorful\",\n            version=\"0.3\",\n        ),\n        make_package(\n            \"fake-tensorboardX\",\n            version=\"0.5\",\n        ),\n        make_package(\n            \"fake-ray\",\n            version=\"0.1\",\n            extras_require={\n                \"default\": [\"fake-colorful==0.3\"],\n                \"tune\": [\"fake-tensorboardX==0.5\"],\n            },\n        ),\n        make_package(\n            \"fake-tune-sklearn\",\n            version=\"0.7\",\n            install_requires=[\n                \"fake-ray[tune]==0.1\",\n            ],\n        ),\n    ]\n\n    dists_dir = tmpdir / \"dists\"\n    for pkg in pkgs:\n        make_wheel(pkg, dists_dir)\n\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.writelines(\n            [\n                \"fake-ray[default]==0.1\\n\",\n                \"fake-tune-sklearn==0.7\\n\",\n            ]\n        )\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--output-file\",\n            \"-\",\n            \"--quiet\",\n            \"--find-links\",\n            str(dists_dir),\n            \"--no-header\",\n            \"--no-emit-options\",\n        ],\n    )\n    assert out.exit_code == 0\n    assert dedent(\"\"\"\\\n        fake-colorful==0.3\n            # via fake-ray\n        fake-ray[default,tune]==0.1\n            # via\n            #   -r requirements.in\n            #   fake-tune-sklearn\n        fake-tensorboardx==0.5\n            # via fake-ray\n        fake-tune-sklearn==0.7\n            # via -r requirements.in\n        \"\"\") == out.stdout\n\n\ndef test_canonicalize_extras(pip_conf, runner, tmp_path, make_package, make_wheel):\n    \"\"\"\n    Ensure extras are written in a consistent format.\n    \"\"\"\n    pkgs = [\n        make_package(\n            \"fake-sqlalchemy\",\n            version=\"0.1\",\n            extras_require={\"fake-postgresql_psycoPG2BINARY\": [\"fake-greenlet\"]},\n        ),\n        make_package(\n            \"fake-greenlet\",\n            version=\"0.2\",\n        ),\n    ]\n\n    dists_dir = tmp_path / \"dists\"\n    for pkg in pkgs:\n        make_wheel(pkg, dists_dir)\n\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"fake-sqlalchemy[FAKE_postgresql-psycopg2binary]\\n\")\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--output-file\",\n            \"-\",\n            \"--find-links\",\n            str(dists_dir),\n            \"--no-header\",\n            \"--no-emit-options\",\n            \"--no-annotate\",\n            \"--no-strip-extras\",\n        ],\n    )\n    assert out.exit_code == 0\n    assert (\n        \"fake-sqlalchemy[fake-postgresql-psycopg2binary]==0.1\"\n        in out.stdout.splitlines()\n    )\n\n\n@pytest.mark.parametrize(\n    (\"pkg2_install_requires\", \"req_in_content\", \"out_expected_content\"),\n    (\n        pytest.param(\n            \"\",\n            [\"test-package-1===0.1.0\\n\"],\n            [\"test-package-1===0.1.0\"],\n            id=\"pin package with ===\",\n        ),\n        pytest.param(\n            \"\",\n            [\"test-package-1==0.1.0\\n\"],\n            [\"test-package-1==0.1.0\"],\n            id=\"pin package with ==\",\n        ),\n        pytest.param(\n            \"test-package-1==0.1.0\",\n            [\"test-package-1===0.1.0\\n\", \"test-package-2==0.1.0\\n\"],\n            [\"test-package-1===0.1.0\", \"test-package-2==0.1.0\"],\n            id=\"dep === pin preferred over == pin, main package == pin\",\n        ),\n        pytest.param(\n            \"test-package-1==0.1.0\",\n            [\"test-package-1===0.1.0\\n\", \"test-package-2===0.1.0\\n\"],\n            [\"test-package-1===0.1.0\", \"test-package-2===0.1.0\"],\n            id=\"dep === pin preferred over == pin, main package === pin\",\n        ),\n        pytest.param(\n            \"test-package-1==0.1.0\",\n            [\"test-package-2===0.1.0\\n\"],\n            [\"test-package-1==0.1.0\", \"test-package-2===0.1.0\"],\n            id=\"dep == pin conserved, main package === pin\",\n        ),\n    ),\n)\ndef test_triple_equal_pinned_dependency_is_used(\n    runner,\n    make_package,\n    make_wheel,\n    tmpdir,\n    pkg2_install_requires,\n    req_in_content,\n    out_expected_content,\n):\n    \"\"\"\n    Test that pip-compile properly emits the pinned requirement with ===\n    torchvision 0.8.2 requires torch==1.7.1 which can resolve to versions with\n    patches (e.g. torch 1.7.1+cu110), we want torch===1.7.1 without patches\n    \"\"\"\n\n    dists_dir = tmpdir / \"dists\"\n\n    test_package_1 = make_package(\"test_package_1\", version=\"0.1.0\")\n    make_wheel(test_package_1, dists_dir)\n\n    test_package_2 = make_package(\n        \"test_package_2\", version=\"0.1.0\", install_requires=[pkg2_install_requires]\n    )\n    make_wheel(test_package_2, dists_dir)\n\n    with open(\"requirements.in\", \"w\") as reqs_in:\n        for line in req_in_content:\n            reqs_in.write(line)\n\n    out = runner.invoke(cli, [\"--find-links\", str(dists_dir)])\n\n    assert out.exit_code == 0, out\n    for line in out_expected_content:\n        assert line in out.stderr\n\n\nMETADATA_TEST_CASES = (\n    pytest.param(\n        \"setup.cfg\",\n        \"\"\"\n            [metadata]\n            name = sample_lib\n            author = Vincent Driessen\n            author_email = me@nvie.com\n\n            [options]\n            packages = find:\n            install_requires = small-fake-a==0.1\n\n            [options.extras_require]\n            dev = small-fake-b==0.2\n            test = small-fake-c==0.3\n        \"\"\",\n        id=\"setup.cfg\",\n    ),\n    pytest.param(\n        \"setup.py\",\n        \"\"\"\n            from setuptools import setup, find_packages\n\n            setup(\n                name=\"sample_lib\",\n                version=0.1,\n                install_requires=[\"small-fake-a==0.1\"],\n                packages=find_packages(),\n                extras_require={\n                    \"dev\": [\"small-fake-b==0.2\"],\n                    \"test\": [\"small-fake-c==0.3\"],\n                },\n            )\n        \"\"\",\n        id=\"setup.py\",\n    ),\n    pytest.param(\n        \"pyproject.toml\",\n        \"\"\"\n            [build-system]\n            requires = [\"flit_core >=2,<4\"]\n            build-backend = \"flit_core.buildapi\"\n\n            [tool.flit.metadata]\n            module = \"sample_lib\"\n            author = \"Vincent Driessen\"\n            author-email = \"me@nvie.com\"\n\n            requires = [\"small-fake-a==0.1\"]\n\n            [tool.flit.metadata.requires-extra]\n            dev  = [\"small-fake-b==0.2\"]\n            test = [\"small-fake-c==0.3\"]\n        \"\"\",\n        id=\"flit\",\n    ),\n    pytest.param(\n        \"pyproject.toml\",\n        \"\"\"\n            [build-system]\n            requires = [\"poetry_core>=1.0.0\"]\n            build-backend = \"poetry.core.masonry.api\"\n\n            [tool.poetry]\n            name = \"sample_lib\"\n            version = \"0.1.0\"\n            description = \"\"\n            authors = [\"Vincent Driessen <me@nvie.com>\"]\n\n            [tool.poetry.dependencies]\n            python = \"*\"\n            small-fake-a = \"0.1\"\n            small-fake-b = \"0.2\"\n            small-fake-c = \"0.3\"\n\n            [tool.poetry.extras]\n            dev  = [\"small-fake-b\"]\n            test = [\"small-fake-c\"]\n        \"\"\",\n        id=\"poetry\",\n    ),\n)\n\n\n@pytest.mark.network\n@pytest.mark.parametrize((\"fname\", \"content\"), METADATA_TEST_CASES)\ndef test_not_specified_input_file(\n    fake_dists, runner, make_module, fname, content, monkeypatch\n):\n    \"\"\"\n    Test that a default-named file is parsed if present.\n    \"\"\"\n    meta_path = make_module(fname=fname, content=content)\n    monkeypatch.chdir(os.path.dirname(meta_path))\n    out = runner.invoke(\n        cli,\n        [\n            \"--output-file\",\n            \"-\",\n            \"--no-header\",\n            \"--no-emit-options\",\n            \"--no-annotate\",\n            \"--no-build-isolation\",\n            \"--find-links\",\n            fake_dists,\n        ],\n    )\n    monkeypatch.undo()\n\n    assert out.exit_code == 0, out.stderr\n    assert \"small-fake-a==0.1\\n\" == out.stdout\n\n\ndef test_not_specified_input_file_without_allowed_files(runner):\n    \"\"\"\n    It should raise an error if there are no input files or default input files\n    such as \"setup.py\" or \"requirements.in\".\n    \"\"\"\n    out = runner.invoke(cli)\n    assert out.exit_code == 2\n    expected_error = (\n        \"Error: Invalid value: If you do not specify an input file, the default \"\n        \"is one of: requirements.in, setup.py, pyproject.toml, setup.cfg\"\n    )\n    assert expected_error in out.stderr.splitlines()\n\n\n@pytest.mark.network\n@pytest.mark.parametrize((\"fname\", \"content\"), METADATA_TEST_CASES)\ndef test_input_formats(fake_dists, runner, make_module, fname, content):\n    \"\"\"\n    Test different dependency formats as input file.\n    \"\"\"\n    meta_path = make_module(fname=fname, content=content)\n    out = runner.invoke(\n        cli, [\"-n\", \"--no-build-isolation\", \"--find-links\", fake_dists, meta_path]\n    )\n    assert out.exit_code == 0, out.stderr\n    assert \"small-fake-a==0.1\" in out.stderr\n    assert \"small-fake-b\" not in out.stderr\n    assert \"small-fake-c\" not in out.stderr\n    assert \"extra ==\" not in out.stderr\n\n\n@pytest.mark.parametrize(\"verbose_option\", (True, False))\ndef test_error_in_pyproject_toml(\n    fake_dists, runner, make_module, capfd, verbose_option\n):\n    \"\"\"\n    Test that an error in pyproject.toml is reported.\n    \"\"\"\n    fname = \"pyproject.toml\"\n    invalid_content = dedent(\"\"\"\\\n        [project]\n        invalid = \"metadata\"\n        \"\"\")\n    meta_path = make_module(fname=fname, content=invalid_content)\n\n    options = []\n    if verbose_option:\n        options = [\"--verbose\"]\n\n    options.extend(\n        [\"-n\", \"--no-build-isolation\", \"--find-links\", fake_dists, meta_path]\n    )\n\n    out = runner.invoke(cli, options)\n\n    assert out.exit_code == 2, out.stderr\n    captured = capfd.readouterr()\n\n    assert (\n        bool(re.search(r\"`project` must contain \\['[^']+'\\] properties\", captured.err))\n    ) is verbose_option\n\n\n@pytest.mark.network\n@pytest.mark.parametrize((\"fname\", \"content\"), METADATA_TEST_CASES)\ndef test_one_extra(fake_dists, runner, make_module, fname, content):\n    \"\"\"\n    Test one ``--extra`` (dev) passed, other extras (test) must be ignored.\n    \"\"\"\n    meta_path = make_module(fname=fname, content=content)\n    out = runner.invoke(\n        cli,\n        [\n            \"-n\",\n            \"--extra\",\n            \"dev\",\n            \"--no-build-isolation\",\n            \"--find-links\",\n            fake_dists,\n            meta_path,\n        ],\n    )\n    assert out.exit_code == 0, out.stderr\n    assert \"small-fake-a==0.1\" in out.stderr\n    assert \"small-fake-b==0.2\" in out.stderr\n    assert \"extra ==\" not in out.stderr\n\n\n@pytest.mark.network\n@pytest.mark.parametrize(\n    \"extra_opts\",\n    (\n        pytest.param((\"--extra\", \"dev\", \"--extra\", \"test\"), id=\"singular\"),\n        pytest.param((\"--extra\", \"dev,test\"), id=\"comma-separated\"),\n    ),\n)\n@pytest.mark.parametrize((\"fname\", \"content\"), METADATA_TEST_CASES)\ndef test_multiple_extras(fake_dists, runner, make_module, fname, content, extra_opts):\n    \"\"\"\n    Test passing multiple ``--extra`` params.\n    \"\"\"\n    meta_path = make_module(fname=fname, content=content)\n    out = runner.invoke(\n        cli,\n        [\n            \"-n\",\n            *extra_opts,\n            \"--no-build-isolation\",\n            \"--find-links\",\n            fake_dists,\n            meta_path,\n        ],\n    )\n    assert out.exit_code == 0, out.stderr\n    assert \"small-fake-a==0.1\" in out.stderr\n    assert \"small-fake-b==0.2\" in out.stderr\n    assert \"extra ==\" not in out.stderr\n\n\n@pytest.mark.network\n@pytest.mark.parametrize((\"fname\", \"content\"), METADATA_TEST_CASES)\ndef test_all_extras(fake_dists, runner, make_module, fname, content):\n    \"\"\"\n    Test passing ``--all-extras`` includes all applicable extras.\n    \"\"\"\n    meta_path = make_module(fname=fname, content=content)\n    out = runner.invoke(\n        cli,\n        [\n            \"--output-file\",\n            \"-\",\n            \"--quiet\",\n            \"--all-extras\",\n            \"--find-links\",\n            fake_dists,\n            \"--no-annotate\",\n            \"--no-emit-options\",\n            \"--no-header\",\n            \"--no-build-isolation\",\n            meta_path,\n        ],\n    )\n    assert out.exit_code == 0, out\n    assert dedent(\"\"\"\\\n            small-fake-a==0.1\n            small-fake-b==0.2\n            small-fake-c==0.3\n            \"\"\") == out.stdout\n\n\n# This should not depend on the metadata format so testing all cases is wasteful\n@pytest.mark.parametrize((\"fname\", \"content\"), METADATA_TEST_CASES[:1])\ndef test_all_extras_fail_with_extra(fake_dists, runner, make_module, fname, content):\n    \"\"\"\n    Test that passing ``--all-extras`` and ``--extra`` fails.\n    \"\"\"\n    meta_path = make_module(fname=fname, content=content)\n    out = runner.invoke(\n        cli,\n        [\n            \"-n\",\n            \"--all-extras\",\n            \"--extra\",\n            \"dev\",\n            \"--find-links\",\n            fake_dists,\n            \"--no-annotate\",\n            \"--no-emit-options\",\n            \"--no-header\",\n            \"--no-build-isolation\",\n            meta_path,\n        ],\n    )\n    assert out.exit_code == 2\n    exp = \"--extra has no effect when used with --all-extras\"\n    assert exp in out.stderr\n\n\ndef _mock_resolver_cls(monkeypatch: pytest.MonkeyPatch) -> MagicMock:\n    obj = MagicMock()\n    obj.resolve = MagicMock(return_value=set())\n    obj.resolve_hashes = MagicMock(return_value=dict())\n    cls = MagicMock(return_value=obj)\n\n    monkeypatch.setattr(\"piptools.scripts.compile.BacktrackingResolver\", cls)\n    monkeypatch.setattr(\"piptools.scripts.compile.LegacyResolver\", cls)\n\n    return cls\n\n\ndef _mock_build_project_metadata(monkeypatch: pytest.MonkeyPatch) -> MagicMock:\n    func = MagicMock(\n        return_value=ProjectMetadata(\n            extras=(\"e\",),\n            requirements=(\n                install_req_from_line(\"rdep0\"),\n                install_req_from_line(\"rdep1; extra=='e'\"),\n            ),\n            build_requirements=(install_req_from_line(\"bdep0\"),),\n        )\n    )\n\n    monkeypatch.setattr(\"piptools.scripts.compile.build_project_metadata\", func)\n\n    return func\n\n\n@backtracking_resolver_only\n@pytest.mark.network\ndef test_all_extras_and_all_build_deps(\n    fake_dists_with_build_deps,\n    runner,\n    tmp_path,\n    monkeypatch,\n    current_resolver,\n):\n    \"\"\"\n    Test that trying to lock all dependencies gives the expected output.\n    \"\"\"\n    src_pkg_path = pathlib.Path(PACKAGES_PATH) / \"small_fake_with_build_deps\"\n    # When used as argument to the runner it is not passed to pip\n    monkeypatch.setenv(\"PIP_FIND_LINKS\", fake_dists_with_build_deps)\n\n    with runner.isolated_filesystem(tmp_path) as tmp_pkg_path:\n        shutil.copytree(src_pkg_path, tmp_pkg_path, dirs_exist_ok=True)\n        out = runner.invoke(\n            cli,\n            [\n                \"--allow-unsafe\",\n                \"--output-file\",\n                \"-\",\n                \"--quiet\",\n                \"--no-emit-options\",\n                \"--no-header\",\n                \"--all-extras\",\n                \"--all-build-deps\",\n            ],\n        )\n\n    assert out.exit_code == 0\n    # Note that the build dependencies of our build dependencies are not resolved.\n    # This means that if our build dependencies are not available as wheels then we will not get\n    # reproducible results.\n    assert \"fake_transient_build_dep\" not in out.stdout\n    assert out.stdout == dedent(\"\"\"\\\n        fake-direct-extra-runtime-dep==0.2\n            # via small-fake-with-build-deps (setup.py)\n        fake-direct-runtime-dep==0.1\n            # via small-fake-with-build-deps (setup.py)\n        fake-dynamic-build-dep-for-all==0.2\n            # via\n            #   small-fake-with-build-deps (pyproject.toml::build-system.backend::editable)\n            #   small-fake-with-build-deps (pyproject.toml::build-system.backend::sdist)\n            #   small-fake-with-build-deps (pyproject.toml::build-system.backend::wheel)\n        fake-dynamic-build-dep-for-editable==0.5\n            # via small-fake-with-build-deps (pyproject.toml::build-system.backend::editable)\n        fake-dynamic-build-dep-for-sdist==0.3\n            # via small-fake-with-build-deps (pyproject.toml::build-system.backend::sdist)\n        fake-dynamic-build-dep-for-wheel==0.4\n            # via small-fake-with-build-deps (pyproject.toml::build-system.backend::wheel)\n        fake-static-build-dep==0.1\n            # via small-fake-with-build-deps (pyproject.toml::build-system.requires)\n        fake-transient-run-dep==0.3\n            # via fake-static-build-dep\n        wheel==0.41.1\n            # via\n            #   small-fake-with-build-deps (pyproject.toml::build-system.backend::wheel)\n            #   small-fake-with-build-deps (pyproject.toml::build-system.requires)\n\n        # The following packages are considered to be unsafe in a requirements file:\n        setuptools==68.1.2\n            # via small-fake-with-build-deps (pyproject.toml::build-system.requires)\n        \"\"\")\n\n\n@backtracking_resolver_only\ndef test_all_build_deps(runner, tmp_path, monkeypatch):\n    \"\"\"\n    Test that ``--all-build-deps`` is equivalent to specifying every\n    ``--build-deps-for``.\n    \"\"\"\n    func = _mock_build_project_metadata(monkeypatch)\n    _mock_resolver_cls(monkeypatch)\n\n    src_file = tmp_path / \"pyproject.toml\"\n    src_file.touch()\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--all-build-deps\",\n            os.fspath(src_file),\n        ],\n    )\n    assert out.exit_code == 0\n    assert func.call_args.kwargs[\"build_targets\"] == (\n        \"editable\",\n        \"sdist\",\n        \"wheel\",\n    )\n\n\n@backtracking_resolver_only\ndef test_only_build_deps(runner, tmp_path, monkeypatch):\n    \"\"\"\n    Test that ``--only-build-deps`` excludes dependencies other than build dependencies.\n    \"\"\"\n    _mock_build_project_metadata(monkeypatch)\n    cls = _mock_resolver_cls(monkeypatch)\n\n    src_file = tmp_path / \"pyproject.toml\"\n    src_file.touch()\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--all-build-deps\",\n            \"--only-build-deps\",\n            os.fspath(src_file),\n        ],\n    )\n    assert out.exit_code == 0\n    assert [c.name for c in cls.call_args.kwargs[\"constraints\"]] == [\"bdep0\"]\n\n\n@backtracking_resolver_only\ndef test_all_build_deps_fail_with_build_target(runner):\n    \"\"\"\n    Test that passing ``--all-build-deps`` and ``--build-deps-for`` fails.\n    \"\"\"\n    out = runner.invoke(\n        cli,\n        [\n            \"--all-build-deps\",\n            \"--build-deps-for\",\n            \"sdist\",\n        ],\n    )\n    exp = \"--build-deps-for has no effect when used with --all-build-deps\"\n    assert out.exit_code == 2\n    assert exp in out.stderr\n\n\n@backtracking_resolver_only\ndef test_only_build_deps_fails_without_any_build_deps(runner):\n    \"\"\"\n    Test that passing ``--only-build-deps`` fails when it is not specified how build deps should\n    be gathered.\n    \"\"\"\n    out = runner.invoke(\n        cli,\n        [\"--only-build-deps\"],\n    )\n    exp = \"--only-build-deps requires either --build-deps-for or --all-build-deps\"\n    assert out.exit_code == 2\n    assert exp in out.stderr\n\n\n@backtracking_resolver_only\n@pytest.mark.parametrize(\"option\", (\"--all-extras\", \"--extra=foo\"))\ndef test_only_build_deps_fails_with_conflicting_options(runner, option):\n    \"\"\"\n    Test that passing ``--all-build-deps`` and conflicting option fails.\n    \"\"\"\n    out = runner.invoke(\n        cli,\n        [\n            \"--all-build-deps\",\n            \"--only-build-deps\",\n            option,\n        ],\n    )\n    exp = \"--only-build-deps cannot be used with any of --extra, --all-extras\"\n    assert out.exit_code == 2\n    assert exp in out.stderr\n\n\n@backtracking_resolver_only\n@pytest.mark.parametrize(\"option\", (\"--all-build-deps\", \"--build-deps-for=wheel\"))\ndef test_build_deps_fail_without_setup_file(runner, tmpdir, option):\n    \"\"\"\n    Test that passing ``--build-deps-for`` or ``--all-build-deps`` fails when used with a\n    requirements file as opposed to a setup file.\n    \"\"\"\n    path = pathlib.Path(tmpdir) / \"requirements.in\"\n    path.write_text(\"\\n\")\n    out = runner.invoke(cli, [\"-n\", option, os.fspath(path)])\n    exp = (\n        \"--build-deps-for and --all-build-deps can be used only with the \"\n        \"setup.py, setup.cfg and pyproject.toml specs.\"\n    )\n    assert out.exit_code == 2\n    assert exp in out.stderr\n\n\ndef test_extras_fail_with_requirements_in(runner, tmpdir):\n    \"\"\"\n    Test that passing ``--extra`` with ``requirements.in`` input file fails.\n    \"\"\"\n    path = pathlib.Path(tmpdir) / \"requirements.in\"\n    path.write_text(\"\\n\")\n    out = runner.invoke(cli, [\"-n\", \"--extra\", \"something\", os.fspath(path)])\n    assert out.exit_code == 2\n    exp = \"--extra has effect only with setup.py and PEP-517 input formats\"\n    assert exp in out.stderr\n\n\ndef test_cli_compile_strip_extras(runner, make_package, make_sdist, tmpdir):\n    \"\"\"\n    Assures that ``--strip-extras`` removes mention of extras from output.\n    \"\"\"\n    test_package_1 = make_package(\n        \"test_package_1\", version=\"0.1\", extras_require={\"more\": \"test_package_2\"}\n    )\n    test_package_2 = make_package(\n        \"test_package_2\",\n        version=\"0.1\",\n    )\n    dists_dir = tmpdir / \"dists\"\n\n    for pkg in (test_package_1, test_package_2):\n        make_sdist(pkg, dists_dir)\n\n    with open(\"requirements.in\", \"w\") as reqs_out:\n        reqs_out.write(\"test_package_1[more]\")\n\n    out = runner.invoke(cli, [\"--strip-extras\", \"--find-links\", str(dists_dir)])\n\n    assert out.exit_code == 0, out\n    assert \"test-package-2==0.1\" in out.stderr\n    assert \"[more]\" not in out.stderr\n\n\ndef test_cli_compile_all_extras_with_multiple_packages(\n    runner, make_package, make_sdist, tmpdir\n):\n    \"\"\"\n    Assures that ``--all-extras`` works when multiple sources are specified.\n    \"\"\"\n    test_package_1 = make_package(\n        \"test_package_1\",\n        version=\"0.1\",\n        extras_require={\"more\": []},\n    )\n    test_package_2 = make_package(\n        \"test_package_2\",\n        version=\"0.1\",\n        extras_require={\"more\": []},\n    )\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--all-extras\",\n            \"--output-file\",\n            \"requirements.txt\",\n            str(test_package_1 / \"setup.py\"),\n            str(test_package_2 / \"setup.py\"),\n        ],\n    )\n\n    assert out.exit_code == 0, out\n    assert \"--all-extras\" in out.stderr\n    assert f\"test_package_1{os.path.sep}0.1{os.path.sep}setup.py\" in out.stderr\n    assert f\"test_package_2{os.path.sep}0.1{os.path.sep}setup.py\" in out.stderr\n\n\n@pytest.mark.parametrize(\n    (\"package_specs\", \"constraints\", \"existing_reqs\", \"expected_reqs\"),\n    (\n        (\n            [\n                {\n                    \"name\": \"test_package_1\",\n                    \"version\": \"1.1\",\n                    \"install_requires\": [\"test_package_2 ~= 1.1\"],\n                },\n                {\n                    \"name\": \"test_package_2\",\n                    \"version\": \"1.1\",\n                    \"extras_require\": {\"more\": \"test_package_3\"},\n                },\n            ],\n            \"\"\"\n            test_package_1 == 1.1\n            \"\"\",\n            \"\"\"\n            test_package_1 == 1.0\n            test_package_2 == 1.0\n            \"\"\",\n            \"\"\"\n            test-package-1==1.1\n            test-package-2==1.1\n            \"\"\",\n        ),\n        (\n            [\n                {\n                    \"name\": \"test_package_1\",\n                    \"version\": \"1.1\",\n                    \"install_requires\": [\"test_package_2[more] ~= 1.1\"],\n                },\n                {\n                    \"name\": \"test_package_2\",\n                    \"version\": \"1.1\",\n                    \"extras_require\": {\"more\": \"test_package_3\"},\n                },\n                {\n                    \"name\": \"test_package_3\",\n                    \"version\": \"0.1\",\n                },\n            ],\n            \"\"\"\n            test_package_1 == 1.1\n            \"\"\",\n            \"\"\"\n            test_package_1 == 1.0\n            test_package_2 == 1.0\n            test_package_3 == 0.1\n            \"\"\",\n            \"\"\"\n            test-package-1==1.1\n            test-package-2==1.1\n            test-package-3==0.1\n            \"\"\",\n        ),\n        (\n            [\n                {\n                    \"name\": \"test_package_1\",\n                    \"version\": \"1.1\",\n                    \"install_requires\": [\"test_package_2[more] ~= 1.1\"],\n                },\n                {\n                    \"name\": \"test_package_2\",\n                    \"version\": \"1.1\",\n                    \"extras_require\": {\"more\": \"test_package_3\"},\n                },\n                {\n                    \"name\": \"test_package_3\",\n                    \"version\": \"0.1\",\n                },\n            ],\n            \"\"\"\n            test_package_1 == 1.1\n            \"\"\",\n            \"\"\"\n            test_package_1 == 1.0\n            test_package_2[more] == 1.0\n            test_package_3 == 0.1\n            \"\"\",\n            \"\"\"\n            test-package-1==1.1\n            test-package-2==1.1\n            test-package-3==0.1\n            \"\"\",\n        ),\n    ),\n    ids=(\"no-extra\", \"extra-stripped-from-existing\", \"with-extra-in-existing\"),\n)\ndef test_resolver_drops_existing_conflicting_constraint(\n    runner,\n    make_package,\n    make_sdist,\n    tmpdir,\n    package_specs,\n    constraints,\n    existing_reqs,\n    expected_reqs,\n) -> None:\n    \"\"\"\n    Test that the resolver will find a solution even if some of the existing\n    (indirect) requirements are incompatible with the new constraints.\n\n    This must succeed even if the conflicting requirement includes some extra,\n    no matter whether the extra is mentioned in the existing requirements\n    or not (cf. `issue #1977 <https://github.com/jazzband/pip-tools/issues/1977>`_).\n    \"\"\"\n    expected_requirements = {line.strip() for line in expected_reqs.splitlines()}\n    dists_dir = tmpdir / \"dists\"\n\n    packages = [make_package(**spec) for spec in package_specs]\n    for pkg in packages:\n        make_sdist(pkg, dists_dir)\n\n    with open(\"requirements.txt\", \"w\") as existing_reqs_out:\n        existing_reqs_out.write(dedent(existing_reqs))\n\n    with open(\"requirements.in\", \"w\") as constraints_out:\n        constraints_out.write(dedent(constraints))\n\n    out = runner.invoke(cli, [\"--strip-extras\", \"--find-links\", str(dists_dir)])\n\n    assert out.exit_code == 0, out\n\n    with open(\"requirements.txt\") as req_txt:\n        req_txt_content = req_txt.read()\n        assert expected_requirements.issubset(req_txt_content.splitlines())\n\n\ndef test_resolution_failure(runner):\n    \"\"\"Test resolution impossible for unknown package.\"\"\"\n    with open(\"requirements.in\", \"w\") as reqs_out:\n        reqs_out.write(\"unknown-package\")\n\n    out = runner.invoke(cli)\n\n    assert out.exit_code != 0, out\n\n\ndef test_resolver_reaches_max_rounds(runner):\n    \"\"\"Test resolver reched max rounds and raises error.\"\"\"\n    with open(\"requirements.in\", \"w\") as reqs_out:\n        reqs_out.write(\"six\")\n\n    out = runner.invoke(cli, [\"--max-rounds\", 0])\n\n    assert out.exit_code != 0, out\n\n\ndef test_preserve_via_requirements_constrained_dependencies_when_run_twice(\n    pip_conf, runner\n):\n    \"\"\"\n    Test that 2 consecutive runs of pip-compile (first with a non-existing requirements.txt file,\n    second with an existing file) produce the same output.\n    \"\"\"\n    with open(\"constraints.txt\", \"w\") as constraints_in:\n        constraints_in.write(\"small-fake-a==0.1\")\n\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.write(\"-c constraints.txt\\nsmall_fake_with_deps\")\n\n    cli_arguments = [\"--no-emit-options\", \"--no-header\"]\n\n    # First run of the command will generate `requirements.txt`, which doesn't yet exist.\n    first_out = runner.invoke(cli, cli_arguments)\n    assert first_out.exit_code == 0, first_out\n\n    with open(\"requirements.txt\") as req_txt:\n        first_output = req_txt.read()\n\n    # Second run of the command will update `requirements.txt`.\n    second_out = runner.invoke(cli, cli_arguments)\n    assert second_out.exit_code == 0, second_out\n\n    with open(\"requirements.txt\") as req_txt:\n        second_output = req_txt.read()\n\n    expected_output = dedent(\"\"\"\\\n        small-fake-a==0.1\n            # via\n            #   -c constraints.txt\n            #   small-fake-with-deps\n        small-fake-with-deps==0.1\n            # via -r requirements.in\n        \"\"\")\n    assert first_output == expected_output\n    assert second_output == expected_output\n\n\ndef test_failure_of_legacy_resolver_prompts_for_backtracking(\n    pip_conf, runner, tmpdir, make_package, make_wheel, current_resolver\n):\n    \"\"\"Test that pip-compile prompts to use the backtracking resolver\"\"\"\n    pkgs = [\n        make_package(\"a\", version=\"0.1\", install_requires=[\"b==0.1\"]),\n        make_package(\"a\", version=\"0.2\", install_requires=[\"b==0.2\"]),\n        make_package(\"b\", version=\"0.1\"),\n        make_package(\"b\", version=\"0.2\"),\n        make_package(\"c\", version=\"1\", install_requires=[\"b==0.1\", \"a\"]),\n    ]\n\n    dists_dir = tmpdir / \"dists\"\n    for pkg in pkgs:\n        make_wheel(pkg, dists_dir)\n\n    with open(\"requirements.in\", \"w\") as req_in:\n        req_in.writelines([\"c\"])\n\n    out = runner.invoke(\n        cli,\n        [\"--resolver\", current_resolver, \"--find-links\", str(dists_dir)],\n    )\n\n    if current_resolver == \"legacy\":\n        assert out.exit_code == 2, out\n        assert \"Consider using backtracking resolver with\" in out.stderr\n    elif current_resolver == \"backtracking\":\n        assert out.exit_code == 0, out\n    else:\n        raise AssertionError(\"unreachable\")\n\n\ndef test_print_deprecation_warning_if_using_legacy_resolver(runner, current_resolver):\n    with open(\"requirements.in\", \"w\"):\n        pass\n\n    out = runner.invoke(cli)\n    assert out.exit_code == 0, out\n\n    expected_warning = \"WARNING: the legacy dependency resolver is deprecated\"\n    if current_resolver == \"legacy\":\n        assert expected_warning in out.stderr\n    else:\n        assert expected_warning not in out.stderr\n\n\n@pytest.mark.parametrize(\n    \"input_filenames\",\n    (\n        pytest.param((\"requirements.txt\",), id=\"one file\"),\n        pytest.param((\"requirements.txt\", \"dev-requirements.in\"), id=\"multiple files\"),\n    ),\n)\ndef test_raise_error_when_input_and_output_filenames_are_matched(\n    runner, tmp_path, input_filenames\n):\n    req_in_paths = []\n    for input_filename in input_filenames:\n        req_in = tmp_path / input_filename\n        req_in.touch()\n        req_in_paths.append(req_in.as_posix())\n\n    req_out = tmp_path / \"requirements.txt\"\n    req_out_path = req_out.as_posix()\n\n    out = runner.invoke(cli, req_in_paths + [\"--output-file\", req_out_path])\n    assert out.exit_code == 2\n\n    expected_error = (\n        f\"Error: input and output filenames must not be matched: {req_out_path}\"\n    )\n    assert expected_error in out.stderr.splitlines()\n\n\n@pytest.mark.network\n@backtracking_resolver_only\ndef test_pass_pip_cache_to_pip_args(tmpdir, runner, current_resolver):\n    cache_dir = tmpdir.mkdir(\"cache_dir\")\n\n    with open(\"requirements.in\", \"w\") as fp:\n        fp.write(\"six==1.15.0\")\n\n    out = runner.invoke(\n        cli, [\"--cache-dir\", str(cache_dir), \"--resolver\", current_resolver]\n    )\n    assert out.exit_code == 0\n    # TODO: Remove hack once testing only on v23.3+\n    if _pip_api.PIP_VERSION >= Version(\"23.3.dev0\"):\n        pip_http_cache_dir = \"http-v2\"\n    else:\n        pip_http_cache_dir = \"http\"\n    assert os.listdir(os.path.join(str(cache_dir), pip_http_cache_dir))\n\n\n@backtracking_resolver_only\ndef test_compile_recursive_extras_static(\n    runner,\n    tmp_path,\n    minimal_wheels_path,\n    current_resolver,\n):\n    (tmp_path / \"pyproject.toml\").write_text(dedent(\"\"\"\n            [project]\n            name = \"foo\"\n            version = \"0.0.1\"\n            dependencies = [\"small-fake-a\"]\n            [project.optional-dependencies]\n            footest = [\"small-fake-b\"]\n            dev = [\"foo[footest]\"]\n            \"\"\"))\n    out = runner.invoke(\n        cli,\n        [\n            \"--no-build-isolation\",\n            \"--no-header\",\n            \"--no-annotate\",\n            \"--no-emit-options\",\n            \"--extra\",\n            \"dev\",\n            \"--find-links\",\n            minimal_wheels_path.as_posix(),\n            os.fspath(tmp_path / \"pyproject.toml\"),\n            \"--output-file\",\n            \"-\",\n        ],\n    )\n    expected = rf\"\"\"foo[footest] @ {tmp_path.as_uri()}\nsmall-fake-a==0.2\nsmall-fake-b==0.3\n\"\"\"\n    try:\n        assert out.exit_code == 0\n        assert expected == out.stdout\n    except Exception:  # pragma: no cover\n        print(out.stdout)\n        print(out.stderr)\n        raise\n\n\n@backtracking_resolver_only\n@pytest.mark.parametrize(\n    \"setuptools_version_info\",\n    (\n        PackageVersionParam(\"setuptools\", \"82.0.0\", \"published-2026-02-08\"),\n        PackageVersionParam(\"setuptools\", \"75.3.0\", \"published-2024-10-29\"),\n    ),\n    ids=str,\n)\ndef test_compile_recursive_extras_build_targets(\n    runner,\n    tmp_path,\n    minimal_wheels_path,\n    current_resolver,\n    setuptools_version_info,\n):\n    (tmp_path / \"pyproject.toml\").write_text(dedent(f\"\"\"\n            [build-system]\n            requires = [\"{setuptools_version_info.as_req()}\"]\n            build-backend = \"setuptools.build_meta\"\n            [project]\n            name = \"foo\"\n            version = \"0.0.1\"\n            dependencies = [\"small-fake-a\"]\n            [project.optional-dependencies]\n            footest = [\"small-fake-b\"]\n            dev = [\"foo[footest]\"]\n            \"\"\"))\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--no-build-isolation\",\n            \"--no-header\",\n            \"--no-annotate\",\n            \"--no-emit-options\",\n            \"--extra\",\n            \"dev\",\n            \"--build-deps-for\",\n            \"wheel\",\n            \"--allow-unsafe\",\n            \"--find-links\",\n            minimal_wheels_path.as_posix(),\n            os.fspath(tmp_path / \"pyproject.toml\"),\n            \"--output-file\",\n            \"-\",\n        ],\n    )\n    expected = rf\"\"\"foo[footest] @ {tmp_path.as_uri()}\nsmall-fake-a==0.2\nsmall-fake-b==0.3\n\n# The following packages are considered to be unsafe in a requirements file:\n{setuptools_version_info.as_req()}\n\"\"\"\n    try:\n        assert out.exit_code == 0\n        assert expected == out.stdout\n    except Exception:  # pragma: no cover\n        print(out.stdout)\n        print(out.stderr)\n        raise\n\n\n@backtracking_resolver_only\n@pytest.mark.network\ndef test_compile_build_targets_setuptools_no_wheel_dep(\n    runner,\n    tmp_path,\n    minimal_wheels_path,\n    current_resolver,\n):\n    \"\"\"Check that user requests apply to build dependencies.\n\n    This verifies that build deps compilation would not use the latest version\n    of an unconstrained build requirements list, when the user requested\n    restricting them.\n\n    It is implemented against `setuptools < 70.1.0` which is known to inject a\n    dependency on `wheel` (newer `setuptools` vendor it). The idea is that\n    `pyproject.toml` does not have an upper bound for `setuptools` but the CLI\n    arg does. And when this works correctly, the `wheel` entry will be included\n    into the resolved output.\n\n    This is a regression test for\n    https://github.com/jazzband/pip-tools/pull/1681#issuecomment-2212541289.\n    \"\"\"\n    (tmp_path / \"pyproject.toml\").write_text(dedent(\"\"\"\n            [project]\n            name = \"foo\"\n            version = \"0.0.1\"\n            dependencies = [\"small-fake-a\"]\n            \"\"\"))\n    (tmp_path / \"constraints.txt\").write_text(\"wheel<0.43\")\n    out = runner.invoke(\n        cli,\n        [\n            \"--build-isolation\",\n            \"--no-header\",\n            \"--no-annotate\",\n            \"--no-emit-options\",\n            \"--extra\",\n            \"dev\",\n            \"--build-deps-for\",\n            \"wheel\",\n            \"--find-links\",\n            minimal_wheels_path.as_posix(),\n            os.fspath(tmp_path / \"pyproject.toml\"),\n            \"--constraint\",\n            os.fspath(tmp_path / \"constraints.txt\"),\n            \"--upgrade-package\",\n            \"setuptools < 70.1.0\",  # setuptools>=70.1.0 doesn't require wheel any more\n            \"--output-file\",\n            \"-\",\n        ],\n        catch_exceptions=True,\n    )\n    expected = r\"\"\"small-fake-a==0.2\nwheel==0.42.0\n\n# The following packages are considered to be unsafe in a requirements file:\n# setuptools\n\"\"\"\n    try:\n        assert out.exit_code == 0\n        assert expected == out.stdout\n    except Exception:  # pragma: no cover\n        print(out.stdout)\n        print(out.stderr)\n        raise\n\n\ndef test_config_option(pip_conf, runner, tmp_path, make_config_file):\n    config_file = make_config_file(\"dry-run\", True)\n\n    req_in = tmp_path / \"requirements.in\"\n    req_in.touch()\n\n    out = runner.invoke(cli, [req_in.as_posix(), \"--config\", config_file.as_posix()])\n\n    assert out.exit_code == 0\n    assert \"Dry-run, so nothing updated\" in out.stderr\n\n\ndef test_default_config_option(pip_conf, runner, make_config_file, tmpdir_cwd):\n    make_config_file(\"dry-run\", True)\n\n    req_in = tmpdir_cwd / \"requirements.in\"\n    req_in.touch()\n\n    out = runner.invoke(cli)\n\n    assert out.exit_code == 0\n    assert \"Dry-run, so nothing updated\" in out.stderr\n\n\ndef test_no_config_option_overrides_config_with_defaults(\n    pip_conf, runner, tmp_path, make_config_file\n):\n    config_file = make_config_file(\"dry-run\", True)\n\n    req_in = tmp_path / \"requirements.in\"\n    req_in.touch()\n\n    out = runner.invoke(\n        cli, [req_in.as_posix(), \"--no-config\", \"--config\", config_file.as_posix()]\n    )\n\n    assert out.exit_code == 0\n    assert \"Dry-run, so nothing updated\" not in out.stderr\n\n\ndef test_raise_error_on_unknown_config_option(\n    pip_conf, runner, tmp_path, make_config_file\n):\n    config_file = make_config_file(\"unknown-option\", True)\n\n    req_in = tmp_path / \"requirements.in\"\n    req_in.touch()\n\n    out = runner.invoke(cli, [req_in.as_posix(), \"--config\", config_file.as_posix()])\n\n    assert out.exit_code == 2\n    assert \"No such config key 'unknown_option'\" in out.stderr\n\n\ndef test_raise_error_on_invalid_config_option(\n    pip_conf, runner, tmp_path, make_config_file\n):\n    config_file = make_config_file(\"dry-run\", [\"invalid\", \"value\"])\n\n    req_in = tmp_path / \"requirements.in\"\n    req_in.touch()\n\n    out = runner.invoke(cli, [req_in.as_posix(), \"--config\", config_file.as_posix()])\n\n    assert out.exit_code == 2\n    assert \"Invalid value for config key 'dry_run': ['invalid', 'value']\" in out.stderr\n\n\n@pytest.mark.parametrize(\"option\", (\"-c\", \"--constraint\"))\ndef test_constraint_option(pip_conf, runner, tmpdir_cwd, make_config_file, option):\n    req_in = tmpdir_cwd / \"requirements.in\"\n    req_in.write_text(\"small-fake-a\")\n\n    constraints_txt = tmpdir_cwd / \"constraints.txt\"\n    constraints_txt.write_text(\"small-fake-a==0.1\")\n\n    out = runner.invoke(\n        cli,\n        [\n            req_in.name,\n            option,\n            constraints_txt.name,\n            \"--output-file\",\n            \"-\",\n            \"--no-header\",\n            \"--no-emit-options\",\n        ],\n    )\n\n    assert out.exit_code == 0\n    assert out.stdout == dedent(\"\"\"\\\n        small-fake-a==0.1\n            # via\n            #   -c constraints.txt\n            #   -r requirements.in\n        \"\"\")\n\n\ndef test_allow_in_config_pip_sync_option(pip_conf, runner, tmp_path, make_config_file):\n    config_file = make_config_file(\"--ask\", True)  # pip-sync's option\n\n    req_in = tmp_path / \"requirements.in\"\n    req_in.touch()\n\n    out = runner.invoke(\n        cli, [req_in.as_posix(), \"--verbose\", \"--config\", config_file.as_posix()]\n    )\n\n    assert out.exit_code == 0\n    assert \"Using pip-tools configuration defaults found\" in out.stderr\n\n\ndef test_use_src_files_from_config_if_option_is_not_specified_from_cli(\n    pip_conf, runner, tmp_path, make_config_file\n):\n    foo_in = tmp_path / \"foo.in\"\n    req_in = tmp_path / \"requirements.in\"\n\n    config_file = make_config_file(\"src-files\", [foo_in.as_posix()])\n\n    req_in.write_text(\"small-fake-a==0.1\", encoding=\"utf-8\")\n    foo_in.write_text(\"small-fake-b==0.1\", encoding=\"utf-8\")\n\n    out = runner.invoke(cli, [\"--config\", config_file.as_posix()])\n\n    assert out.exit_code == 0, out\n    assert \"small-fake-b\" in out.stderr\n    assert \"small-fake-a\" not in out.stderr\n\n\ndef test_use_src_files_from_cli_if_option_is_specified_in_both_config_and_cli(\n    pip_conf, runner, tmp_path, make_config_file\n):\n    foo_in = tmp_path / \"foo.in\"\n    req_in = tmp_path / \"requirements.in\"\n\n    config_file = make_config_file(\"src-files\", [foo_in.as_posix()])\n\n    req_in.write_text(\"small-fake-a==0.1\", encoding=\"utf-8\")\n    foo_in.write_text(\"small-fake-b==0.1\", encoding=\"utf-8\")\n\n    out = runner.invoke(cli, [req_in.as_posix(), \"--config\", config_file.as_posix()])\n\n    assert out.exit_code == 0, out\n    assert \"small-fake-a\" in out.stderr\n    assert \"small-fake-b\" not in out.stderr\n\n\ndef test_cli_boolean_flag_config_option_has_valid_context(\n    pip_conf, runner, tmp_path, make_config_file\n):\n    config_file = make_config_file(\"no-annotate\", True)\n\n    req_in = tmp_path / \"requirements.in\"\n    req_in.write_text(\"small-fake-a==0.1\")\n    out = runner.invoke(\n        cli,\n        [\n            req_in.as_posix(),\n            \"--config\",\n            config_file.as_posix(),\n            \"--no-emit-options\",\n            \"--no-header\",\n            \"--output-file\",\n            \"-\",\n        ],\n    )\n    assert out.exit_code == 0\n    assert out.stdout == \"small-fake-a==0.1\\n\"\n\n\ndef test_invalid_cli_boolean_flag_config_option_captured(\n    pip_conf, runner, tmp_path, make_config_file\n):\n    config_file = make_config_file(\"no-annnotate\", True)\n\n    req_in = tmp_path / \"requirements.in\"\n    req_in.touch()\n\n    out = runner.invoke(cli, [req_in.as_posix(), \"--config\", config_file.as_posix()])\n\n    assert out.exit_code == 2\n    assert \"No such config key 'annnotate'.\" in out.stderr\n\n\nstrip_extras_warning = (\n    \"WARNING: --strip-extras is becoming the default in version 8.0.0.\"\n)\n\n\ndef test_show_warning_on_default_strip_extras_option(\n    runner, make_package, make_sdist, tmp_path\n):\n    req_in = tmp_path / \"requirements.in\"\n    req_in.touch()\n\n    out = runner.invoke(cli, req_in.as_posix())\n\n    assert out.exit_code == 0\n    assert strip_extras_warning in out.stderr\n\n\n@pytest.mark.parametrize(\"option\", (\"--strip-extras\", \"--no-strip-extras\"))\ndef test_do_not_show_warning_on_explicit_strip_extras_option(\n    runner, make_package, make_sdist, tmp_path, option\n):\n    req_in = tmp_path / \"requirements.in\"\n    req_in.touch()\n\n    out = runner.invoke(cli, [option, req_in.as_posix()])\n\n    assert out.exit_code == 0\n    assert strip_extras_warning not in out.stderr\n\n\ndef test_origin_of_extra_requirement_not_written_to_annotations(\n    pip_conf, runner, make_package, make_wheel, tmp_path, tmpdir\n):\n    req_in = tmp_path / \"requirements.in\"\n    package_with_extras = make_package(\n        \"package_with_extras\",\n        version=\"0.1\",\n        extras_require={\n            \"extra1\": [\"small-fake-a==0.1\"],\n            \"extra2\": [\"small-fake-b==0.1\"],\n        },\n    )\n\n    dists_dir = tmpdir / \"dists\"\n    make_wheel(package_with_extras, dists_dir)\n\n    with open(req_in, \"w\") as req_out:\n        req_out.write(\"package-with-extras[extra1,extra2]\")\n\n    out = runner.invoke(\n        cli,\n        [\n            \"--output-file\",\n            \"-\",\n            \"--quiet\",\n            \"--no-header\",\n            \"--find-links\",\n            str(dists_dir),\n            \"--no-emit-options\",\n            \"--no-build-isolation\",\n            req_in.as_posix(),\n        ],\n    )\n\n    assert out.exit_code == 0, out\n    assert dedent(f\"\"\"\\\n        package-with-extras[extra1,extra2]==0.1\n            # via -r {req_in.as_posix()}\n        small-fake-a==0.1\n            # via package-with-extras\n        small-fake-b==0.1\n            # via package-with-extras\n        \"\"\") == out.stdout\n\n\ndef test_tool_specific_config_option(pip_conf, runner, tmp_path, make_config_file):\n    config_file = make_config_file(\n        \"dry-run\", True, section=\"pip-tools\", subsection=\"compile\"\n    )\n\n    req_in = tmp_path / \"requirements.in\"\n    req_in.touch()\n\n    out = runner.invoke(cli, [req_in.as_posix(), \"--config\", config_file.as_posix()])\n\n    assert out.exit_code == 0\n    assert \"Dry-run, so nothing updated\" in out.stderr\n\n\n@pytest.mark.xfail(reason=\"https://github.com/jazzband/pip-tools/issues/2012\")\n@mock.patch(\"piptools.scripts.compile.parse_requirements\")\ndef test_stdout_should_not_be_read_when_stdin_is_not_a_plain_file(\n    parse_req,\n    runner,\n    tmp_path,\n):\n    parse_req.side_effect = lambda fname, finder, options, session: pytest.fail(\n        \"Must not be called when output is a fifo\"\n    )\n\n    req_in = tmp_path / \"requirements.txt\"\n    req_in.touch()\n\n    fifo = tmp_path / \"fifo\"\n\n    os.mkfifo(fifo)\n\n    out = runner.invoke(cli, [req_in.as_posix(), \"--output-file\", fifo.as_posix()])\n\n    assert out.exit_code == 0, out\n\n\n@pytest.mark.parametrize(\n    \"input_path_absolute\", (True, False), ids=(\"absolute-input\", \"relative-input\")\n)\n@pytest.mark.parametrize(\n    \"test_files_collection\",\n    (\n        FileCollectionParam(\n            \"relative_include\",\n            {\n                \"requirements2.in\": \"small-fake-a\\n\",\n                \"requirements.in\": \"-r requirements2.in\\n\",\n            },\n        ),\n        FileCollectionParam(\n            \"absolute_include\",\n            {\n                \"requirements2.in\": \"small-fake-a\\n\",\n                \"requirements.in\": lambda tmpdir: f\"-r {(tmpdir / 'requirements2.in').as_posix()}\",\n            },\n        ),\n    ),\n    ids=str,\n)\ndef test_second_order_requirements_path_handling(\n    pip_conf,\n    runner,\n    tmp_path,\n    monkeypatch,\n    pip_produces_absolute_paths,\n    input_path_absolute,\n    test_files_collection,\n):\n    \"\"\"\n    Test normalization of ``-r`` includes in output.\n\n    Given nested requirements files, the internal requirements file path will\n    be written in the output, and it will be absolute or relative depending\n    only on whether or not the initial path was absolute or relative.\n    \"\"\"\n    test_files_collection.populate(tmp_path)\n\n    # the input path is given on the CLI as absolute or relative\n    # and this determines the expected output path as well\n    input_dir_path = tmp_path if input_path_absolute else pathlib.Path(\".\")\n    input_path = (input_dir_path / \"requirements.in\").as_posix()\n    output_path = (input_dir_path / \"requirements2.in\").as_posix()\n\n    with monkeypatch.context() as revertable_ctx:\n        revertable_ctx.chdir(tmp_path)\n\n        out = runner.invoke(\n            cli,\n            [\n                \"--output-file\",\n                \"-\",\n                \"--quiet\",\n                \"--no-header\",\n                \"--no-emit-options\",\n                \"-r\",\n                input_path,\n            ],\n        )\n\n    assert out.exit_code == 0\n    assert out.stdout == dedent(f\"\"\"\\\n        small-fake-a==0.2\n            # via -r {output_path}\n        \"\"\")\n\n\n@pytest.mark.parametrize(\n    \"test_files_collection\",\n    (\n        FileCollectionParam(\n            \"parent_dir\",\n            {\n                \"requirements2.in\": \"small-fake-a\\n\",\n                \"subdir/requirements.in\": \"-r ../requirements2.in\\n\",\n            },\n        ),\n        FileCollectionParam(\n            \"subdir\",\n            {\n                \"requirements.in\": \"-r ./subdir/requirements2.in\",\n                \"subdir/requirements2.in\": \"small-fake-a\\n\",\n            },\n        ),\n        FileCollectionParam(\n            \"sibling_dir\",\n            {\n                \"subdir1/requirements.in\": \"-r ../subdir2/requirements2.in\",\n                \"subdir2/requirements2.in\": \"small-fake-a\\n\",\n            },\n        ),\n    ),\n    ids=str,\n)\ndef test_second_order_requirements_relative_path_in_separate_dir(\n    pip_conf,\n    runner,\n    tmp_path,\n    monkeypatch,\n    test_files_collection,\n    pip_produces_absolute_paths,\n):\n    \"\"\"\n    Test normalization of ``-r`` includes when the requirements files are in\n    distinct directories.\n\n    Confirm that the output path will be relative to the current working\n    directory.\n    \"\"\"\n    test_files_collection.populate(tmp_path)\n    # the input is the path to 'requirements.in' relative to the starting dir\n    input_path = test_files_collection.get_path_to(\"requirements.in\")\n    # the output should also be relative to the starting dir, the path to 'requirements2.in'\n    output_path = test_files_collection.get_path_to(\"requirements2.in\")\n\n    # for older pip versions, recompute the output path to be relative to the input path\n    if not pip_produces_absolute_paths:\n        # traverse upwards to the root tmp dir, and append the output path to that\n        # similar to pathlib.Path.relative_to(..., walk_up=True)\n        relative_segments = len(pathlib.Path(input_path).parents) - 1\n        output_path = (\n            pathlib.Path(input_path).parent / (\"../\" * relative_segments) / output_path\n        ).as_posix()\n\n    with monkeypatch.context() as revertable_ctx:\n        revertable_ctx.chdir(tmp_path)\n        out = runner.invoke(\n            cli,\n            [\n                \"--output-file\",\n                \"-\",\n                \"--quiet\",\n                \"--no-header\",\n                \"--no-emit-options\",\n                \"-r\",\n                input_path,\n            ],\n        )\n\n    assert out.exit_code == 0\n    assert out.stdout == dedent(f\"\"\"\\\n        small-fake-a==0.2\n            # via -r {output_path}\n        \"\"\")\n\n\ndef test_second_order_requirements_can_be_in_parent_of_cwd(\n    pip_conf,\n    runner,\n    tmp_path,\n    monkeypatch,\n    pip_produces_absolute_paths,\n):\n    \"\"\"\n    Test handling of ``-r`` includes when the included requirements file is in the\n    parent of the current working directory.\n    \"\"\"\n    test_files_collection = FileCollectionParam(\n        contents={\n            \"subdir1/requirements.in\": \"-r ../requirements2.in\\n\",\n            \"requirements2.in\": \"small-fake-a\\n\",\n        }\n    )\n    test_files_collection.populate(tmp_path)\n\n    with monkeypatch.context() as revertable_ctx:\n        # cd into the subdir where the initial requirements are\n        revertable_ctx.chdir(tmp_path / \"subdir1\")\n        out = runner.invoke(\n            cli,\n            [\n                \"--output-file\",\n                \"-\",\n                \"--quiet\",\n                \"--no-header\",\n                \"--no-emit-options\",\n                \"-r\",\n                \"requirements.in\",\n            ],\n        )\n\n    assert out.exit_code == 0\n    assert out.stdout == dedent(\"\"\"\\\n        small-fake-a==0.2\n            # via -r ../requirements2.in\n        \"\"\")\n\n\n@pytest.mark.parametrize(\n    \"input_path_absolute\", (True, False), ids=(\"absolute-input\", \"relative-input\")\n)\ndef test_url_constraints_are_not_treated_as_file_paths(\n    pip_conf,\n    make_package,\n    runner,\n    tmp_path,\n    monkeypatch,\n    input_path_absolute,\n):\n    \"\"\"\n    Test normalization of ``-c`` constraints when the constraints are HTTPS URLs.\n    The constraints should be preserved verbatim.\n\n    This is a regression test for\n    https://github.com/jazzband/pip-tools/issues/2223\n    \"\"\"\n    constraints_url = \"https://example.com/files/common_constraints.txt\"\n\n    reqs_in = tmp_path / \"requirements.in\"\n    reqs_in.write_text(f\"\"\"\n        small-fake-a\n        -c {constraints_url}\n        \"\"\")\n\n    input_dir_path = tmp_path if input_path_absolute else pathlib.Path(\".\")\n    input_path = (input_dir_path / \"requirements.in\").as_posix()\n\n    # TODO: find a better way of mocking the callout to get the constraints\n    #       file (use `responses`?)\n    #\n    # we need a mock response for `GET https://...` as fetched by pip\n    # although this is fragile, it can be adapted if pip changes\n    def fake_url_get(url):\n        response = mock.Mock()\n        response.reason = \"Ok\"\n        response.status_code = 200\n        response.url = url\n        response.text = \"small-fake-a==0.2\"\n        return response\n\n    mock_get = mock.Mock(side_effect=fake_url_get)\n\n    with monkeypatch.context() as revertable_ctx:\n        revertable_ctx.chdir(tmp_path)\n        revertable_ctx.setattr(PipSession, \"get\", mock_get)\n        out = runner.invoke(\n            cli,\n            [\n                \"--output-file\",\n                \"-\",\n                \"--quiet\",\n                \"--no-header\",\n                \"--no-emit-options\",\n                \"-r\",\n                input_path,\n            ],\n        )\n\n    # sanity check, pip should have tried to fetch the constraints\n    mock_get.assert_called_once_with(constraints_url)\n\n    assert out.exit_code == 0\n    assert out.stdout == dedent(f\"\"\"\\\n        small-fake-a==0.2\n            # via\n            #   -c {constraints_url}\n            #   -r {input_path}\n        \"\"\")\n\n\n@pytest.mark.parametrize(\n    \"pyproject_path_is_absolute\",\n    (True, False),\n    ids=(\"absolute-input\", \"relative-input\"),\n)\ndef test_that_self_referential_pyproject_toml_extra_can_be_compiled(\n    pip_conf, runner, tmp_path, monkeypatch, pyproject_path_is_absolute\n):\n    \"\"\"\n    Test that a :file:`pyproject.toml` source file can use self-referential extras\n    which point back to the original package name.\n\n    This is a regression test for:\n    https://github.com/jazzband/pip-tools/issues/2215\n    \"\"\"\n    src_file = tmp_path / \"pyproject.toml\"\n    src_file.write_text(dedent(\"\"\"\n            [project]\n            name = \"foo\"\n            version = \"0.1.0\"\n            [project.optional-dependencies]\n            ext1 = [\"small-fake-a\"]\n            ext2 = [\"foo[ext1]\"]\n            \"\"\"))\n\n    if pyproject_path_is_absolute:\n        input_path = src_file.as_posix()\n    else:\n        input_path = src_file.relative_to(tmp_path).as_posix()\n\n    with monkeypatch.context() as revertable_ctx:\n        revertable_ctx.chdir(tmp_path)\n        out = runner.invoke(\n            cli,\n            [\n                \"--output-file\",\n                \"-\",\n                \"--quiet\",\n                \"--no-header\",\n                \"--no-emit-options\",\n                # use in-process setuptools\n                \"--no-build-isolation\",\n                # importantly, request the extra which uses the self-reference\n                \"--extra\",\n                \"ext2\",\n                input_path,\n            ],\n        )\n\n    assert out.exit_code == 0\n    assert out.stdout == dedent(f\"\"\"\\\n        foo[ext1] @ {src_file.parent.absolute().as_uri()}\n            # via foo ({input_path})\n        small-fake-a==0.2\n            # via foo\n        \"\"\")\n\n\ndef test_compile_with_generate_hashes_preserves_extra_index_url(\n    pip_with_index_conf,\n    minimal_wheels_path,\n    runner,\n    tmpdir_cwd,\n):\n    \"\"\"\n    Regression test for\n    https://github.com/jazzband/pip-tools/issues/2220\n\n    Using ``--generate-hashes`` triggers the codepath which clears the package finder\n    cache (``allow_all_wheels()``), and that code incorrectly cleared more information\n    than desired, removing extra index URLs in addition to cached package info.\n    \"\"\"\n    reqs_in = tmpdir_cwd / \"requirements.in\"\n    reqs_in.write_text(dedent(\"\"\"\\\n            --extra-index-url http://extraindex1.com\n\n            small-fake-a\n            \"\"\"))\n\n    out = runner.invoke(\n        cli,\n        [\"--output-file\", \"-\", \"--no-header\", \"--strip-extras\", \"--generate-hashes\"],\n    )\n\n    # the output should contain\n    # - the `--index-url` from the pip config\n    # - the `--extra-index-url` from `requirements.in`\n    # - the `--find-links` option from pip config\n    #\n    # and then package resolution information\n    assert out.stdout == dedent(f\"\"\"\\\n        --index-url http://example.com\n        --extra-index-url http://extraindex1.com\n        --find-links {minimal_wheels_path.as_posix()}\n\n        small-fake-a==0.2 \\\\\n            --hash=sha256:33e1acdca3b9162e002cedb0e58b350d731d1ed3f53a6b22e0a628bca7c7c6ed\n            # via -r requirements.in\n        \"\"\")\n"
  },
  {
    "path": "tests/test_cli_sync.py",
    "content": "from __future__ import annotations\n\nimport os\nimport subprocess\nimport sys\nfrom unittest import mock\n\nimport pytest\nfrom pip._vendor.packaging.version import Version\n\nfrom piptools.scripts import sync\nfrom piptools.scripts.sync import cli\n\n\n@pytest.fixture(autouse=True)\ndef _temp_default_reqs(tmp_path, monkeypatch):\n    monkeypatch.setattr(\n        sync, \"DEFAULT_REQUIREMENTS_FILE\", str(tmp_path / \"requirements.txt\")\n    )\n\n\ndef test_run_as_module_sync():\n    \"\"\"piptools can be run as ``python -m piptools ...``.\"\"\"\n\n    result = subprocess.run(\n        [sys.executable, \"-m\", \"piptools\", \"sync\", \"--help\"],\n        stdout=subprocess.PIPE,\n        check=True,\n    )\n\n    # Should have run pip-sync successfully.\n    assert result.stdout.startswith(b\"Usage:\")\n    assert b\"Synchronize virtual environment with\" in result.stdout\n\n\ndef test_sync_help_opt_supports_short_and_long_flag(runner):\n    shortflag_result = runner.invoke(cli, [\"-h\"])\n    longflag_result = runner.invoke(cli, [\"--help\"])\n    assert shortflag_result.exit_code == 0\n    assert longflag_result.exit_code == 0\n\n    assert shortflag_result.stdout.startswith(\"Usage:\")\n    assert longflag_result.stdout.startswith(\"Usage:\")\n    assert shortflag_result.stdout == longflag_result.stdout\n\n\ndef test_sync_help_opt_shows_examples_section(runner):\n    result = runner.invoke(cli, [\"-h\"])\n    assert result.exit_code == 0\n    assert result.stdout.startswith(\"Usage:\")\n\n    # not only should there be an `Examples` section in the output, but it should have\n    # no preceding whitespace where it is shown\n    assert \"\\nExamples:\\n\" in result.stdout\n\n\n@mock.patch(\"piptools.sync.run\")\ndef test_quiet_option(run, runner):\n    \"\"\"sync command can be run with `--quiet` or `-q` flag.\"\"\"\n\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as req_in:\n        req_in.write(\"six==1.10.0\")\n\n    out = runner.invoke(cli, [\"-q\"])\n    assert not out.stderr_bytes\n    assert out.exit_code == 0\n\n    # for every call to pip ensure the `-q` flag is set\n    assert run.call_count == 2\n    for call in run.call_args_list:\n        assert \"-q\" in call[0][0]\n\n\n@mock.patch(\"piptools.sync.run\")\ndef test_quiet_option_when_up_to_date(run, runner):\n    \"\"\"\n    Sync should output nothing when everything is up to date and quiet option is set.\n    \"\"\"\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\"):\n        pass\n\n    with mock.patch(\"piptools.sync.diff\", return_value=(set(), set())):\n        out = runner.invoke(cli, [\"-q\"])\n\n    assert not out.stderr_bytes\n    assert out.exit_code == 0\n    run.assert_not_called()\n\n\ndef test_no_requirements_file(runner):\n    \"\"\"\n    It should raise an error if there are no input files\n    and a requirements.txt file does not exist.\n    \"\"\"\n    out = runner.invoke(cli)\n\n    assert \"No requirement files given\" in out.stderr\n    assert out.exit_code == 2\n\n\ndef test_input_files_with_dot_in_extension(runner, tmp_path):\n    \"\"\"\n    It should raise an error if some of the input files have .in extension.\n    \"\"\"\n    req_in = tmp_path / \"requirements.in\"\n    req_in.write_text(\"six==1.10.0\")\n\n    out = runner.invoke(cli, [str(req_in)])\n\n    assert \"ERROR: Some input files have the .in extension\" in out.stderr\n    assert out.exit_code == 2\n\n\ndef test_force_files_with_dot_in_extension(runner, tmp_path):\n    \"\"\"\n    It should print a warning and sync anyway if some of the input files\n    have .in extension.\n    \"\"\"\n    req_in = tmp_path / \"requirements.in\"\n    req_in.write_text(\"six==1.10.0\")\n\n    with mock.patch(\"piptools.sync.run\"):\n        out = runner.invoke(cli, [str(req_in), \"--force\"])\n\n    assert \"WARNING: Some input files have the .in extension\" in out.stderr\n    assert out.exit_code == 0\n\n\n@pytest.mark.parametrize(\n    (\"req_lines\", \"should_raise\"),\n    (\n        ([\"six>1.10.0\", \"six<1.10.0\"], True),\n        (\n            [\"six>1.10.0 ; python_version>='3.0'\", \"six<1.10.0 ; python_version<'3.0'\"],\n            False,\n        ),\n    ),\n)\ndef test_merge_error(req_lines, should_raise, runner):\n    \"\"\"\n    Sync command should raise an error if there are merge errors.\n    It should not raise an error if otherwise incompatible requirements\n    are isolated by exclusive environment markers.\n    \"\"\"\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as req_in:\n        for line in req_lines:\n            req_in.write(line + \"\\n\")\n\n    with mock.patch(\"piptools.sync.run\"):\n        out = runner.invoke(cli, [\"-n\"])\n\n    if should_raise:\n        assert out.exit_code == 2\n        assert \"Incompatible requirements found\" in out.stderr\n    else:\n        assert out.exit_code == 1\n\n\n@pytest.mark.parametrize(\n    \"req_line\",\n    (\n        \"file:.\",\n        \"-e file:.\",\n    ),\n)\n@mock.patch(\"piptools.sync.run\")\ndef test_merge_no_name_urls(run, req_line, runner, tmp_path):\n    \"\"\"\n    Test sync succeeds when merging requirements that lack names.\n    \"\"\"\n    reqs_paths = [\n        (tmp_path / name) for name in (\"requirements.txt\", \"dev_requirements.txt\")\n    ]\n\n    for reqs_path in reqs_paths:\n        reqs_path.write_text(f\"{req_line} \\n\")\n\n    out = runner.invoke(cli, [str(path) for path in reqs_paths])\n    assert out.exit_code == 0\n    assert run.call_count == 2\n\n\n@pytest.mark.parametrize(\n    (\"cli_flags\", \"expected_install_flags\"),\n    (\n        (\n            [\"--find-links\", \"./libs1\", \"--find-links\", \"./libs2\"],\n            [\"--find-links\", \"./libs1\", \"--find-links\", \"./libs2\"],\n        ),\n        ([\"--no-index\"], [\"--no-index\"]),\n        (\n            [\"--index-url\", \"https://example.com\"],\n            [\"--index-url\", \"https://example.com\"],\n        ),\n        (\n            [\"--extra-index-url\", \"https://foo\", \"--extra-index-url\", \"https://bar\"],\n            [\"--extra-index-url\", \"https://foo\", \"--extra-index-url\", \"https://bar\"],\n        ),\n        (\n            [\"--trusted-host\", \"foo\", \"--trusted-host\", \"bar\"],\n            [\"--trusted-host\", \"foo\", \"--trusted-host\", \"bar\"],\n        ),\n        ([\"--user\"], [\"--user\"]),\n        ([\"--cert\", \"foo.crt\"], [\"--cert\", \"foo.crt\"]),\n        ([\"--client-cert\", \"foo.pem\"], [\"--client-cert\", \"foo.pem\"]),\n        (\n            [\"--pip-args\", \"--no-cache-dir --no-deps --no-warn-script-location\"],\n            [\"--no-cache-dir\", \"--no-deps\", \"--no-warn-script-location\"],\n        ),\n        ([\"--pip-args='--cache-dir=/tmp'\"], [\"--cache-dir=/tmp\"]),\n        (\n            [\"--pip-args=\\\"--cache-dir='/tmp/cache dir with spaces/'\\\"\"],\n            [\"--cache-dir='/tmp/cache dir with spaces/'\"],\n        ),\n    ),\n)\n@mock.patch(\"piptools.sync.run\")\ndef test_pip_install_flags(run, cli_flags, expected_install_flags, runner):\n    \"\"\"\n    Test the cli flags have to be passed to the pip install command.\n    \"\"\"\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as req_in:\n        req_in.write(\"six==1.10.0\")\n\n    runner.invoke(cli, cli_flags)\n\n    call_args = [call[0][0] for call in run.call_args_list]\n    called_install_options = [args[6:] for args in call_args if args[3] == \"install\"]\n    assert called_install_options == [expected_install_flags], \"Called args: {}\".format(\n        call_args\n    )\n\n\n@pytest.mark.parametrize(\n    \"install_flags\",\n    (\n        [\"--no-index\"],\n        [\"--index-url\", \"https://example.com\"],\n        [\"--extra-index-url\", \"https://example.com\"],\n        [\"--find-links\", \"./libs1\"],\n        [\"--trusted-host\", \"example.com\"],\n        [\"--no-binary\", \":all:\"],\n        [\"--only-binary\", \":all:\"],\n    ),\n)\n@mock.patch(\"piptools.sync.run\")\ndef test_pip_install_flags_in_requirements_file(run, runner, install_flags):\n    \"\"\"\n    Test the options from requirements.txt file pass to the pip install command.\n    \"\"\"\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as reqs:\n        reqs.write(\" \".join(install_flags) + \"\\n\")\n        reqs.write(\"six==1.10.0\")\n\n    out = runner.invoke(cli)\n    assert out.exit_code == 0, out\n\n    # Make sure pip install command has expected options\n    call_args = [call[0][0] for call in run.call_args_list]\n    called_install_options = [args[6:] for args in call_args if args[3] == \"install\"]\n    assert called_install_options == [install_flags], f\"Called args: {call_args}\"\n\n\n@mock.patch(\"piptools.sync.run\")\ndef test_sync_ask_declined(run, runner):\n    \"\"\"\n    Make sure nothing is installed if the confirmation is declined\n    \"\"\"\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as req_in:\n        req_in.write(\"small-fake-a==1.10.0\")\n\n    runner.invoke(cli, [\"--ask\"], input=\"n\\n\")\n\n    run.assert_not_called()\n\n\n@mock.patch(\"piptools.sync.run\")\ndef test_sync_ask_accepted(run, runner):\n    \"\"\"\n    Make sure pip is called when the confirmation is accepted (even if\n    --dry-run is given)\n    \"\"\"\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as req_in:\n        req_in.write(\"small-fake-a==1.10.0\")\n\n    runner.invoke(cli, [\"--ask\", \"--dry-run\"], input=\"y\\n\")\n\n    assert run.call_count == 2\n\n\ndef test_sync_dry_run_returns_non_zero_exit_code(runner):\n    \"\"\"\n    Make sure non-zero exit code is returned when --dry-run is given.\n    \"\"\"\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as req_in:\n        req_in.write(\"small-fake-a==1.10.0\")\n\n    out = runner.invoke(cli, [\"--dry-run\"])\n\n    assert out.exit_code == 1\n\n\n@mock.patch(\"piptools.sync.run\")\ndef test_python_executable_option(\n    run,\n    runner,\n    fake_dist,\n):\n    \"\"\"\n    Make sure sync command can run with `--python-executable` option.\n    \"\"\"\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as req_in:\n        req_in.write(\"small-fake-a==1.10.0\")\n\n    custom_executable = os.path.abspath(sys.executable)\n\n    runner.invoke(cli, [\"--python-executable\", custom_executable])\n\n    assert run.call_count == 2\n\n    call_args = [call[0][0] for call in run.call_args_list]\n    called_uninstall_options = [\n        args[:5] for args in call_args if args[3] == \"uninstall\"\n    ]\n    called_install_options = [args[:-1] for args in call_args if args[3] == \"install\"]\n\n    assert called_uninstall_options == [\n        [custom_executable, \"-m\", \"pip\", \"uninstall\", \"-y\"]\n    ]\n    assert called_install_options == [[custom_executable, \"-m\", \"pip\", \"install\", \"-r\"]]\n\n\n@pytest.mark.parametrize(\n    \"python_executable\",\n    (\n        \"/tmp/invalid_executable\",\n        \"invalid_python\",\n    ),\n)\ndef test_invalid_python_executable(runner, python_executable):\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as req_in:\n        req_in.write(\"small-fake-a==1.10.0\")\n\n    out = runner.invoke(cli, [\"--python-executable\", python_executable])\n    assert out.exit_code == 2, out\n    message = \"Could not resolve '{}' as valid executable path or alias.\\n\"\n    assert out.stderr == message.format(python_executable)\n\n\n@mock.patch(\"piptools._internal._pip_api.get_pip_version_for_python_executable\")\ndef test_invalid_pip_version_in_python_executable(\n    get_pip_version_for_python_executable, runner, tmp_path\n):\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as req_in:\n        req_in.write(\"small-fake-a==1.10.0\")\n\n    # a dummy executable on Windows needs to end in `.exe` in order for\n    # `shutil.which` to find it\n    custom_executable = tmp_path / \"custom_executable.exe\"\n    custom_executable.write_text(\"\")\n\n    custom_executable.chmod(0o700)\n\n    get_pip_version_for_python_executable.return_value = Version(\"19.1\")\n\n    out = runner.invoke(cli, [\"--python-executable\", str(custom_executable)])\n    assert out.exit_code == 2, out\n    message = (\n        \"Target python executable '{}' has pip version 19.1 installed. \"\n        \"Version\"  # \">=20.3 is expected.\\n\" part is omitted\n    )\n    assert out.stderr.startswith(message.format(custom_executable))\n\n\n@mock.patch(\"piptools.sync.run\")\ndef test_default_python_executable_option(run, runner):\n    \"\"\"\n    Make sure sys.executable is used when --python-executable is not provided.\n    \"\"\"\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as req_in:\n        req_in.write(\"small-fake-a==1.10.0\")\n\n    runner.invoke(cli)\n\n    assert run.call_count == 2\n\n    call_args = [call[0][0] for call in run.call_args_list]\n    called_install_options = [args[:-1] for args in call_args if args[3] == \"install\"]\n    assert called_install_options == [\n        [\n            sys.executable,\n            \"-m\",\n            \"pip\",\n            \"install\",\n            \"-r\",\n        ]\n    ]\n\n\n@mock.patch(\"piptools.sync.run\")\ndef test_default_config_option(run, runner, make_config_file, tmpdir_cwd):\n    make_config_file(\"dry-run\", True)\n\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as reqs_txt:\n        reqs_txt.write(\"six==1.10.0\")\n\n    out = runner.invoke(cli)\n\n    assert out.exit_code == 1\n    assert \"Would install:\" in out.stdout\n\n\n@mock.patch(\"piptools.sync.run\")\ndef test_config_option(run, runner, make_config_file):\n    config_file = make_config_file(\"dry-run\", True)\n\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as reqs_txt:\n        reqs_txt.write(\"six==1.10.0\")\n\n    out = runner.invoke(cli, [\"--config\", config_file.as_posix()])\n\n    assert out.exit_code == 1\n    assert \"Would install:\" in out.stdout\n\n\n@mock.patch(\"piptools.sync.run\")\ndef test_no_config_option_overrides_config_with_defaults(run, runner, make_config_file):\n    config_file = make_config_file(\"dry-run\", True)\n\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as reqs_txt:\n        reqs_txt.write(\"six==1.10.0\")\n\n    out = runner.invoke(cli, [\"--no-config\", \"--config\", config_file.as_posix()])\n\n    assert out.exit_code == 0\n    assert \"Would install:\" not in out.stdout\n\n\n@mock.patch(\"piptools.sync.run\")\ndef test_raise_error_on_unknown_config_option(run, runner, tmp_path, make_config_file):\n    config_file = make_config_file(\"unknown-option\", True)\n\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as reqs_txt:\n        reqs_txt.write(\"six==1.10.0\")\n\n    out = runner.invoke(cli, [\"--config\", config_file.as_posix()])\n\n    assert out.exit_code == 2\n    assert \"No such config key 'unknown_option'\" in out.stderr\n\n\n@mock.patch(\"piptools.sync.run\")\ndef test_raise_error_on_invalid_config_option(run, runner, tmp_path, make_config_file):\n    config_file = make_config_file(\"dry-run\", [\"invalid\", \"value\"])\n\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as reqs_txt:\n        reqs_txt.write(\"six==1.10.0\")\n\n    out = runner.invoke(cli, [\"--config\", config_file.as_posix()])\n\n    assert out.exit_code == 2\n    assert \"Invalid value for config key 'dry_run': ['invalid', 'value']\" in out.stderr\n\n\n@mock.patch(\"piptools.sync.run\")\ndef test_allow_in_config_pip_compile_option(run, runner, tmp_path, make_config_file):\n    config_file = make_config_file(\"generate-hashes\", True)  # pip-compile's option\n\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as reqs_txt:\n        reqs_txt.write(\"six==1.10.0\")\n\n    out = runner.invoke(cli, [\"--verbose\", \"--config\", config_file.as_posix()])\n\n    assert out.exit_code == 0\n    assert \"Using pip-tools configuration defaults found\" in out.stderr\n\n\n@mock.patch(\"piptools.sync.run\")\ndef test_tool_specific_config_option(run, runner, make_config_file):\n    config_file = make_config_file(\n        \"dry-run\", True, section=\"pip-tools\", subsection=\"sync\"\n    )\n\n    with open(sync.DEFAULT_REQUIREMENTS_FILE, \"w\") as reqs_txt:\n        reqs_txt.write(\"six==1.10.0\")\n\n    out = runner.invoke(cli, [\"--config\", config_file.as_posix()])\n\n    assert out.exit_code == 1\n    assert \"Would install:\" in out.stdout\n"
  },
  {
    "path": "tests/test_data/fake-editables.json",
    "content": "{\n  \"git+git://example.org/django.git#egg=django\": [],\n  \"git+https://github.com/celery/billiard#egg=billiard\": []\n}\n"
  },
  {
    "path": "tests/test_data/fake-index.json",
    "content": "{\n  \"aiohttp\": {\n    \"3.6.2\": { \"\": [\"yarl\"] }\n  },\n  \"anyjson\": {\n    \"0.3.3\": { \"\": [] }\n  },\n  \"amqp\": {\n    \"1.4.9\": { \"\": [] },\n    \"2.0.2\": { \"\": [\"vine>=1.1.1\"] },\n    \"2.1.4\": { \"\": [\"vine>=1.1.3\"] }\n  },\n  \"appdirs\": {\n    \"1.4.9\": { \"\": [] }\n  },\n  \"arrow\": {\n    \"0.5.0\": { \"\": [\"python-dateutil\"] },\n    \"0.5.4\": { \"\": [\"python-dateutil\"] }\n  },\n  \"billiard\": {\n    \"3.3.0.23\": { \"\": [] },\n    \"3.5.0.2\": { \"\": [] }\n  },\n  \"celery\": {\n    \"3.1.18\": {\n      \"\": [\"kombu<3.1,>=3.0.25\", \"pytz>0.dev.0\", \"billiard<3.4,>=3.3.0.20\"]\n    },\n    \"3.1.23\": { \"\": [\"kombu>=3.0.34,<4\", \"pytz>0.dev.0\", \"billiard>=3.3.0.23\"] },\n    \"4.0.2\": {\n      \"\": [\"kombu<5.0,>=4.0.2\", \"pytz>0.dev.0\", \"billiard<3.6.0,>=3.5.0.2\"]\n    }\n  },\n  \"click\": {\n    \"3.3\": { \"\": [] },\n    \"4.0\": { \"\": [] }\n  },\n  \"django\": {\n    \"1.6.11\": { \"\": [] },\n    \"1.7.7\": { \"\": [] },\n    \"1.8\": { \"\": [] }\n  },\n  \"fake-piptools-test-with-pinned-deps\": {\n    \"0.1\": { \"\": [\"celery==3.1.18\"] }\n  },\n  \"fake-piptools-test-with-unsafe-deps\": {\n    \"0.1\": { \"\": [\"setuptools==34.0.0\"] }\n  },\n  \"flask\": {\n    \"0.10.1\": { \"\": [\"Jinja2>=2.4\", \"Werkzeug>=0.7\", \"itsdangerous>=0.21\"] }\n  },\n  \"flask-cors\": {\n    \"1.10.2\": { \"\": [\"Flask>=0.9\", \"Six\"] },\n    \"2.0.0\": { \"\": [\"Flask>=0.9\", \"Six\"] }\n  },\n  \"gnureadline\": {\n    \"6.3.3\": { \"\": [] }\n  },\n  \"html5lib\": {\n    \"0.999999999\": { \"\": [\"setuptools>=18.5\"] }\n  },\n  \"idna\": {\n    \"2.8\": { \"\": [] }\n  },\n  \"ipython\": {\n    \"2.1.0\": {\n      \"\": [\"gnureadline\"],\n      \"nbconvert\": [\"pygments\", \"jinja2\", \"Sphinx>=0.3\"],\n      \"notebook\": [\"tornado>=3.1\", \"pyzmq>=2.1.11\", \"jinja2\"]\n    }\n  },\n  \"itsdangerous\": {\n    \"0.24\": { \"\": [] }\n  },\n  \"jinja2\": {\n    \"2.7.3\": { \"\": [\"markupsafe\"] }\n  },\n  \"kombu\": {\n    \"3.0.35\": { \"\": [\"anyjson>=0.3.3\", \"amqp>=1.4.9,<2.0\"] },\n    \"4.0.2\": { \"\": [\"amqp<3.0,>=2.1.4\"] }\n  },\n  \"librabbitmq\": {\n    \"1.6.1\": { \"\": [\"amqp>=1.4.6\"] }\n  },\n  \"markupsafe\": {\n    \"0.23\": { \"\": [] }\n  },\n  \"packaging\": {\n    \"16.8\": { \"\": [] }\n  },\n  \"psycopg2\": {\n    \"2.5.4\": { \"\": [] },\n    \"2.6\": { \"\": [] }\n  },\n  \"pygments\": {\n    \"1.5\": { \"\": [] }\n  },\n  \"pyzmq\": {\n    \"2.1.12\": { \"\": [] }\n  },\n  \"pytz\": {\n    \"2016.4\": { \"\": [] }\n  },\n  \"setuptools\": {\n    \"34.0.0\": { \"\": [\"packaging>=16.8\", \"appdirs>=1.4.0\"] },\n    \"35.0.0\": { \"\": [] }\n  },\n  \"six\": {\n    \"1.6.1\": { \"\": [] },\n    \"1.9.0\": { \"\": [] }\n  },\n  \"sphinx\": {\n    \"0.3\": { \"\": [] }\n  },\n  \"sqlalchemy\": {\n    \"0.9.8\": { \"\": [] },\n    \"0.9.9\": { \"\": [] },\n    \"1.0.0b5\": { \"\": [] }\n  },\n  \"tornado\": {\n    \"3.2.2\": { \"\": [] }\n  },\n  \"vine\": {\n    \"1.1.1\": { \"\": [] },\n    \"1.1.3\": { \"\": [] }\n  },\n  \"werkzeug\": {\n    \"0.6\": { \"\": [] },\n    \"0.10\": { \"\": [] },\n    \"0.10.4\": { \"\": [] }\n  },\n  \"yarl\": {\n    \"1.4.2\": { \"\": [\"idna\"] }\n  }\n}\n"
  },
  {
    "path": "tests/test_data/packages/fake_with_deps/fake_with_deps/__init__.py",
    "content": ""
  },
  {
    "path": "tests/test_data/packages/fake_with_deps/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"fake_with_deps\"\ndescription = \"Fake package with dependencies\"\nauthors = [{name = \"jazzband\"}]\n# license = \"0BSD\"\n# Deprecated license table for Python 3.8's latest setuptools compatibility:\nlicense = {text = \"0BSD\"}\nversion = \"0.1\"\ndependencies = [\n  \"python-dateutil>=2.4.2,<2.5\",\n  \"colorama<0.4.0,>=0.3.7\",\n  \"cornice<1.1,>=1.0.0\",\n  \"enum34<1.1.7,>=1.0.4\",\n  \"six>1.5,<=1.8\",\n  \"ipaddress<1.1,>=1.0.16\",\n  \"jsonschema<3.0,>=2.4.0\",\n  \"pyramid<1.6,>=1.5.7\",\n  \"pyzmq<26.3.0,>=26.2.0\",\n  \"simplejson>=3.5,!=3.8,>3.9\",\n  \"SQLAlchemy!=0.9.5,<2.0.0,>=0.7.8,>=1.0.0\",\n  \"python-memcached>=1.57,<2.0\",\n  \"xmltodict<=0.11,>=0.4.6\"\n]\n"
  },
  {
    "path": "tests/test_data/packages/small_fake_a/setup.py",
    "content": "from __future__ import annotations\n\nfrom setuptools import setup\n\nsetup(name=\"small_fake_a\", version=0.1)\n"
  },
  {
    "path": "tests/test_data/packages/small_fake_with_build_deps/backend/backend.py",
    "content": "from __future__ import annotations\n\n# A dependency of the build backend that is not installed is equivalent to a build\n# backend that is not installed so we don't have to test both cases.\nimport fake_static_build_dep  # noqa: F401\nimport setuptools.build_meta\n\n# Re-export all names in case more hooks are added in the future\nfrom setuptools.build_meta import *  # noqa: F401, F403\n\nbuild_wheel = setuptools.build_meta.build_wheel\nbuild_sdist = setuptools.build_meta.build_sdist\n\n\ndef get_requires_for_build_sdist(config_settings=None):\n    result = setuptools.build_meta.get_requires_for_build_sdist(config_settings)\n    assert result == []\n    result.append(\"fake_dynamic_build_dep_for_all\")\n    result.append(\"fake_dynamic_build_dep_for_sdist\")\n    return result\n\n\ndef get_requires_for_build_wheel(config_settings=None):\n    result = setuptools.build_meta.get_requires_for_build_wheel(config_settings)\n    assert result == [\"wheel\"]\n    result.append(\"fake_dynamic_build_dep_for_all\")\n    result.append(\"fake_dynamic_build_dep_for_wheel\")\n    return result\n\n\ndef get_requires_for_build_editable(config_settings=None):\n    return [\"fake_dynamic_build_dep_for_all\", \"fake_dynamic_build_dep_for_editable\"]\n"
  },
  {
    "path": "tests/test_data/packages/small_fake_with_build_deps/pyproject.toml",
    "content": "[build-system]\nrequires = [\n    \"setuptools==68.1.2\",\n    \"wheel==0.41.1\",\n    \"fake_static_build_dep\"\n]\nbuild-backend = \"backend\"\nbackend-path = [\"backend\"]\n"
  },
  {
    "path": "tests/test_data/packages/small_fake_with_build_deps/setup.py",
    "content": "from __future__ import annotations\n\nfrom setuptools import setup\n\nsetup(\n    name=\"small_fake_with_build_deps\",\n    version=0.1,\n    install_requires=[\n        \"fake_direct_runtime_dep\",\n    ],\n    extras_require={\n        \"x\": [\"fake_direct_extra_runtime_dep\"],\n    },\n)\n"
  },
  {
    "path": "tests/test_data/packages/small_fake_with_deps/setup.py",
    "content": "from __future__ import annotations\n\nfrom setuptools import setup\n\nsetup(\n    name=\"small_fake_with_deps\",\n    version=0.1,\n    install_requires=[\"small-fake-a==0.1\", \"small-fake-b==0.1\"],\n)\n"
  },
  {
    "path": "tests/test_data/packages/small_fake_with_deps_and_sub_deps/setup.py",
    "content": "from __future__ import annotations\n\nfrom setuptools import setup\n\nsetup(\n    name=\"small_fake_with_deps_and_sub_deps\",\n    version=0.1,\n    install_requires=[\"small-fake-with-unpinned-deps\"],\n)\n"
  },
  {
    "path": "tests/test_data/packages/small_fake_with_pyproject/pyproject.toml",
    "content": "[project]\nname=\"small_fake_with_pyproject\"\nversion=0.1\ndependencies=[\n    \"fake_direct_runtime_dep\",\n]\n[project.optional-dependencies]\nx = [\"fake_direct_extra_runtime_dep[with_its_own_extra]\"]\n"
  },
  {
    "path": "tests/test_data/packages/small_fake_with_subdir/subdir/setup.py",
    "content": "from __future__ import annotations\n\nfrom setuptools import setup\n\nsetup(name=\"small_fake_a\", version=0.1)\n"
  },
  {
    "path": "tests/test_data/packages/small_fake_with_unpinned_deps/setup.py",
    "content": "from __future__ import annotations\n\nfrom setuptools import setup\n\nsetup(\n    name=\"small_fake_with_unpinned_deps\",\n    version=0.1,\n    install_requires=[\"small-fake-a\", \"small-fake-b\"],\n)\n"
  },
  {
    "path": "tests/test_fake_index.py",
    "content": "from __future__ import annotations\n\nimport pytest\n\n\ndef test_find_best_match(from_line, repository):\n    ireq = from_line(\"django>1.5\")\n    assert str(repository.find_best_match(ireq)) == \"django==1.8\"\n\n    ireq = from_line(\"django<1.8,~=1.6\")\n    assert str(repository.find_best_match(ireq)) == \"django==1.7.7\"\n\n    # Extras available, but no extras specified\n    ireq = from_line(\"ipython\")\n    assert str(repository.find_best_match(ireq)) == \"ipython==2.1.0\"\n\n    # Make sure we include extras. They should be sorted in the output.\n    ireq = from_line(\"ipython[notebook,nbconvert]\")\n    assert str(repository.find_best_match(ireq)) == \"ipython[nbconvert,notebook]==2.1.0\"\n\n\ndef test_find_best_match_incl_prereleases(from_line, repository):\n    ireq = from_line(\"SQLAlchemy\")\n    assert (\n        str(repository.find_best_match(ireq, prereleases=False)) == \"sqlalchemy==0.9.9\"\n    )\n    assert (\n        str(repository.find_best_match(ireq, prereleases=True)) == \"sqlalchemy==1.0.0b5\"\n    )\n\n\ndef test_find_best_match_for_editable(from_editable, repository):\n    ireq = from_editable(\"git+git://whatev.org/blah.git#egg=flask\")\n    assert repository.find_best_match(ireq) == ireq\n\n\ndef test_get_dependencies(from_line, repository):\n    ireq = from_line(\"django==1.6.11\")\n    assert repository.get_dependencies(ireq) == []\n\n    ireq = from_line(\"Flask==0.10.1\")\n    dependencies = repository.get_dependencies(ireq)\n    assert {str(req) for req in dependencies} == {\n        \"Werkzeug>=0.7\",\n        \"Jinja2>=2.4\",\n        \"itsdangerous>=0.21\",\n    }\n\n    ireq = from_line(\"ipython==2.1.0\")\n    dependencies = repository.get_dependencies(ireq)\n    assert {str(req) for req in dependencies} == {\"gnureadline\"}\n\n    ireq = from_line(\"ipython[notebook]==2.1.0\")\n    dependencies = repository.get_dependencies(ireq)\n    assert {str(req) for req in dependencies} == {\n        \"gnureadline\",\n        \"pyzmq>=2.1.11\",\n        \"tornado>=3.1\",\n        \"jinja2\",\n    }\n\n    ireq = from_line(\"ipython[notebook,nbconvert]==2.1.0\")\n    dependencies = repository.get_dependencies(ireq)\n    assert {str(req) for req in dependencies} == {\n        \"gnureadline\",\n        \"pyzmq>=2.1.11\",\n        \"tornado>=3.1\",\n        \"jinja2\",\n        \"pygments\",\n        \"Sphinx>=0.3\",\n    }\n\n\ndef test_get_dependencies_for_editable(from_editable, repository):\n    ireq = from_editable(\"git+git://example.org/django.git#egg=django\")\n    assert repository.get_dependencies(ireq) == []\n\n\ndef test_get_dependencies_rejects_non_pinned_requirements(from_line, repository):\n    not_a_pinned_req = from_line(\"django>1.6\")\n    with pytest.raises(TypeError):\n        repository.get_dependencies(not_a_pinned_req)\n\n\ndef test_get_hashes(from_line, repository):\n    ireq = from_line(\"django==1.8\")\n    expected = {\n        \"test:123\",\n        \"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\n    }\n    assert repository.get_hashes(ireq) == expected\n"
  },
  {
    "path": "tests/test_logging.py",
    "content": "from __future__ import annotations\n\nfrom piptools.logging import LogContext\n\n\ndef test_indentation(runner):\n    \"\"\"\n    Test LogContext.indentation() context manager increases indentation.\n    \"\"\"\n    log = LogContext(indent_width=2)\n\n    with runner.isolation() as streams:\n        log.log(\"Test message 1\")\n        with log.indentation():\n            log.log(\"Test message 2\")\n            with log.indentation():\n                log.log(\"Test message 3\")\n            log.log(\"Test message 4\")\n        log.log(\"Test message 5\")\n\n        stderr_bytes = streams[1].getvalue()\n\n    assert stderr_bytes.decode().splitlines() == [\n        \"Test message 1\",\n        \"  Test message 2\",\n        \"    Test message 3\",\n        \"  Test message 4\",\n        \"Test message 5\",\n    ]\n"
  },
  {
    "path": "tests/test_minimal_upgrade.py",
    "content": "from __future__ import annotations\n\nimport pytest\n\nfrom piptools.repositories import LocalRequirementsRepository\nfrom piptools.utils import key_from_ireq\n\n\n@pytest.mark.parametrize(\n    (\"input\", \"pins\", \"expected\"),\n    (\n        (tup)\n        for tup in [\n            # Add Flask to an existing requirements.in, using --no-upgrade\n            (\n                [\"flask\", \"jinja2\", \"werkzeug\"],\n                [\n                    # The requirements.txt from a previous round\n                    \"jinja2==2.7.3\",\n                    \"markupsafe==0.23\",\n                    \"werkzeug==0.6\",\n                ],\n                [\n                    # Add flask and upgrade werkzeug from incompatible 0.6\n                    \"flask==0.10.1\",\n                    \"itsdangerous==0.24 (from flask==0.10.1)\",\n                    \"werkzeug==0.10.4\",\n                    # Other requirements are unchanged from\n                    # the original requirements.txt\n                    \"jinja2==2.7.3\",\n                    \"markupsafe==0.23 (from jinja2==2.7.3)\",\n                ],\n            )\n        ]\n    ),\n)\ndef test_no_upgrades(base_resolver, repository, from_line, input, pins, expected):\n    input = [from_line(line) for line in input]\n    existing_pins = {}\n    for line in pins:\n        ireq = from_line(line)\n        existing_pins[key_from_ireq(ireq)] = ireq\n    local_repository = LocalRequirementsRepository(existing_pins, repository)\n    output = base_resolver(\n        input, prereleases=False, repository=local_repository\n    ).resolve()\n    output = {str(line) for line in output}\n    assert output == {str(line) for line in expected}\n"
  },
  {
    "path": "tests/test_pip_compat.py",
    "content": "from __future__ import annotations\n\nfrom pathlib import Path, PurePosixPath\n\nfrom piptools._compat.pip_compat import parse_requirements\n\nfrom .constants import PACKAGES_RELATIVE_PATH\n\n\ndef test_parse_requirements_preserve_editable_relative_path(tmp_path, repository):\n    test_package_path = str(\n        PurePosixPath(Path(PACKAGES_RELATIVE_PATH)) / \"small_fake_a\"\n    )\n    requirements_in_path = str(tmp_path / \"requirements.in\")\n\n    with open(requirements_in_path, \"w\") as requirements_in_file:\n        requirements_in_file.write(f\"-e {test_package_path}\")\n\n    [install_requirement] = parse_requirements(\n        requirements_in_path, session=repository.session\n    )\n\n    assert install_requirement.link.url == test_package_path\n    assert install_requirement.link.file_path == test_package_path\n"
  },
  {
    "path": "tests/test_repository_local.py",
    "content": "from __future__ import annotations\n\nimport pytest\n\nfrom piptools.repositories.local import LocalRequirementsRepository\nfrom piptools.utils import key_from_ireq\n\nEXPECTED = {\"sha256:5e6071ee6e4c59e0d0408d366fe9b66781d2cf01be9a6e19a2433bb3c5336330\"}\n\n\ndef test_get_hashes_local_repository_cache_miss(\n    capsys, pip_conf, from_line, pypi_repository\n):\n    existing_pins = {}\n    local_repository = LocalRequirementsRepository(existing_pins, pypi_repository)\n    with local_repository.allow_all_wheels():\n        hashes = local_repository.get_hashes(from_line(\"small-fake-a==0.1\"))\n        assert hashes == EXPECTED\n    captured = capsys.readouterr()\n    assert captured.out == \"\"\n    assert captured.err == \"\"\n\n\ndef test_get_hashes_local_repository_cache_hit(from_line, repository):\n    # Create an install requirement with the hashes included in its options\n    hash_options = {\"sha256\": [entry.split(\":\")[1] for entry in EXPECTED]}\n    req = from_line(\"small-fake-a==0.1\", hash_options=hash_options)\n    existing_pins = {key_from_ireq(req): req}\n\n    # Use fake repository so that we know the hashes are coming from cache\n    local_repository = LocalRequirementsRepository(existing_pins, repository)\n    with local_repository.allow_all_wheels():\n        hashes = local_repository.get_hashes(from_line(\"small-fake-a==0.1\"))\n        assert hashes == EXPECTED\n\n\nNONSENSE = {\"sha256:NONSENSE\"}\n\n\n@pytest.mark.parametrize(\n    (\"reuse_hashes\", \"expected\"), ((True, NONSENSE), (False, EXPECTED))\n)\ndef test_toggle_reuse_hashes_local_repository(\n    capsys, pip_conf, from_line, pypi_repository, reuse_hashes, expected\n):\n    # Create an install requirement with the hashes included in its options\n    hash_options = {\"sha256\": [entry.split(\":\")[1] for entry in NONSENSE]}\n    req = from_line(\"small-fake-a==0.1\", hash_options=hash_options)\n    existing_pins = {key_from_ireq(req): req}\n\n    local_repository = LocalRequirementsRepository(\n        existing_pins, pypi_repository, reuse_hashes=reuse_hashes\n    )\n    with local_repository.allow_all_wheels():\n        assert local_repository.get_hashes(from_line(\"small-fake-a==0.1\")) == expected\n    captured = capsys.readouterr()\n    assert captured.out == \"\"\n    assert captured.err == \"\"\n"
  },
  {
    "path": "tests/test_repository_pypi.py",
    "content": "from __future__ import annotations\n\nimport os\nfrom unittest import mock\n\nimport pytest\nfrom pip._internal.models.candidate import InstallationCandidate\nfrom pip._internal.models.link import Link\nfrom pip._internal.utils.urls import path_to_url\nfrom pip._vendor.requests import HTTPError, Session\n\nfrom piptools.repositories import PyPIRepository\nfrom piptools.repositories.pypi import open_local_or_remote_file\n\n\ndef test_generate_hashes_all_platforms(capsys, pip_conf, from_line, pypi_repository):\n    expected = {\n        \"sha256:8d4d131cd05338e09f461ad784297efea3652e542c5fabe04a62358429a6175e\",\n        \"sha256:ad05e1371eb99f257ca00f791b755deb22e752393eb8e75bc01d651715b02ea9\",\n        \"sha256:24afa5b317b302f356fd3fc3b1cfb0aad114d509cf635ea9566052424191b944\",\n        \"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\n    }\n\n    ireq = from_line(\"small-fake-multi-arch==0.1\")\n\n    # pip caches the candidates for the current system, which means\n    # allow_all_wheels won't have the desired effect unless the cache is\n    # cleared. See GH-1532\n    assert pypi_repository.get_hashes(ireq) < expected\n\n    with pypi_repository.allow_all_wheels():\n        assert pypi_repository.get_hashes(ireq) == expected\n    captured = capsys.readouterr()\n    assert captured.out == \"\"\n    assert captured.err == \"\"\n\n\n@pytest.mark.network\ndef test_get_file_hash_without_interfering_with_each_other(from_line, pypi_repository):\n    \"\"\"\n    The PyPIRepository._get_file_hash() used to call unpack_url(),\n    when generating the hash. Unpacking both packages to the same directory\n    will then fail. E.g. matplotlib-2.0.2.tar.gz has a directory named LICENSE,\n    but many other packages have a file named LICENSE.\n\n    See GH-512 and GH-544.\n    \"\"\"\n    assert (\n        pypi_repository._get_file_hash(\n            Link(\n                \"https://files.pythonhosted.org/packages/\"\n                \"f5/f0/9da3ef24ea7eb0ccd12430a261b66eca36b924aeef06e17147f9f9d7d310/\"\n                \"matplotlib-2.0.2.tar.gz\"\n            )\n        )\n        == \"sha256:0ffbc44faa34a8b1704bc108c451ecf87988f900ef7ce757b8e2e84383121ff1\"\n    )\n\n    assert (\n        pypi_repository._get_file_hash(\n            Link(\n                \"https://files.pythonhosted.org/packages/\"\n                \"a1/32/e3d6c3a8b5461b903651dd6ce958ed03c093d2e00128e3f33ea69f1d7965/\"\n                \"cffi-1.9.1.tar.gz\"\n            )\n        )\n        == \"sha256:563e0bd53fda03c151573217b3a49b3abad8813de9dd0632e10090f6190fdaf8\"\n    )\n\n\ndef test_get_hashes_editable_empty_set(from_editable, pypi_repository):\n    ireq = from_editable(\"git+https://github.com/django/django.git#egg=django\")\n    assert pypi_repository.get_hashes(ireq) == set()\n\n\ndef test_get_hashes_unpinned_raises(from_line, pypi_repository):\n    # Under normal pip-tools usage, get_hashes() should never be called with an\n    # unpinned requirement. The TypeError represents a programming mistake.\n    ireq = from_line(\"django\")\n    with pytest.raises(TypeError, match=r\"^Expected pinned requirement, got django\"):\n        pypi_repository.get_hashes(ireq)\n\n\n@pytest.mark.parametrize((\"content\", \"content_length\"), ((b\"foo\", 3), (b\"foobar\", 6)))\ndef test_open_local_or_remote_file__local_file(tmp_path, content, content_length):\n    \"\"\"\n    Test the `open_local_or_remote_file` returns a context manager to a FileStream\n    for a given `Link` to a local file.\n    \"\"\"\n    local_file_path = tmp_path / \"foo.txt\"\n    local_file_path.write_bytes(content)\n\n    link = Link(local_file_path.as_uri())\n    session = Session()\n\n    with open_local_or_remote_file(link, session) as file_stream:\n        assert file_stream.stream.read() == content\n        assert file_stream.size == content_length\n\n\ndef test_open_local_or_remote_file__directory(tmpdir):\n    \"\"\"\n    Test the `open_local_or_remote_file` raises a ValueError for a given `Link`\n    to a directory.\n    \"\"\"\n    link = Link(path_to_url(tmpdir.strpath))\n    session = Session()\n\n    with (\n        pytest.raises(ValueError, match=\"Cannot open directory for read\"),\n        open_local_or_remote_file(link, session),\n    ):\n        pass  # pragma: no cover\n\n\n@pytest.mark.parametrize(\n    (\"content\", \"content_length\", \"expected_content_length\"),\n    ((b\"foo\", 3, 3), (b\"bar\", None, None), (b\"kek\", \"invalid-content-length\", None)),\n)\ndef test_open_local_or_remote_file__remote_file(\n    tmp_path, content, content_length, expected_content_length\n):\n    \"\"\"\n    Test the `open_local_or_remote_file` returns a context manager to a FileStream\n    for a given `Link` to a remote file.\n    \"\"\"\n    link = Link(\"https://example.com/foo.txt\")\n    session = Session()\n\n    response_file_path = tmp_path / \"foo.txt\"\n    response_file_path.write_bytes(content)\n\n    mock_response = mock.Mock()\n    with response_file_path.open(\"rb\") as fp:\n        mock_response.raw = fp\n        mock_response.headers = {\"content-length\": content_length}\n\n        with mock.patch.object(session, \"get\", return_value=mock_response):\n            with open_local_or_remote_file(link, session) as file_stream:\n                assert file_stream.stream.read() == content\n                assert file_stream.size == expected_content_length\n\n        mock_response.close.assert_called_once()\n\n\ndef test_relative_path_cache_dir_is_normalized(from_line):\n    relative_cache_dir = \"pypi-repo-cache\"\n    pypi_repository = PyPIRepository([], cache_dir=relative_cache_dir)\n\n    assert os.path.isabs(pypi_repository._cache_dir)\n    assert pypi_repository._cache_dir.endswith(relative_cache_dir)\n\n\ndef test_relative_path_pip_cache_dir_is_normalized(from_line, tmpdir):\n    relative_cache_dir = \"pip-cache\"\n    pypi_repository = PyPIRepository(\n        [\"--cache-dir\", relative_cache_dir], cache_dir=(tmpdir / \"pypi-repo-cache\")\n    )\n\n    assert os.path.isabs(pypi_repository.options.cache_dir)\n    assert pypi_repository.options.cache_dir.endswith(relative_cache_dir)\n\n\ndef test_pip_cache_dir_is_empty(from_line, tmpdir):\n    pypi_repository = PyPIRepository(\n        [\"--no-cache-dir\"], cache_dir=(tmpdir / \"pypi-repo-cache\")\n    )\n\n    assert not pypi_repository.options.cache_dir\n\n\n@pytest.mark.parametrize(\n    (\"project_data\", \"expected_hashes\"),\n    (\n        pytest.param(\n            {\n                \"releases\": {\n                    \"0.1\": [\n                        {\n                            \"packagetype\": \"bdist_wheel\",\n                            \"digests\": {\"sha256\": \"fake-hash\"},\n                            \"url\": \"https://link\",\n                        }\n                    ]\n                }\n            },\n            {\"https://link\": \"sha256:fake-hash\"},\n            id=\"return single hash\",\n        ),\n        pytest.param(\n            {\n                \"releases\": {\n                    \"0.1\": [\n                        {\n                            \"packagetype\": \"bdist_wheel\",\n                            \"digests\": {\"sha256\": \"fake-hash-number1\"},\n                            \"url\": \"https://link1\",\n                        },\n                        {\n                            \"packagetype\": \"sdist\",\n                            \"digests\": {\"sha256\": \"fake-hash-number2\"},\n                            \"url\": \"https://link2\",\n                        },\n                    ]\n                }\n            },\n            {\n                \"https://link1\": \"sha256:fake-hash-number1\",\n                \"https://link2\": \"sha256:fake-hash-number2\",\n            },\n            id=\"return multiple hashes\",\n        ),\n        pytest.param(\n            {\n                \"releases\": {\n                    \"0.1\": [\n                        {\n                            \"packagetype\": \"bdist_wheel\",\n                            \"digests\": {\"sha256\": \"fake-hash-number1\"},\n                            \"url\": \"https://link1\",\n                        },\n                        {\n                            \"packagetype\": \"sdist\",\n                            \"digests\": {\"sha256\": \"fake-hash-number2\"},\n                            \"url\": \"https://link2\",\n                        },\n                        {\n                            \"packagetype\": \"bdist_eggs\",\n                            \"digests\": {\"sha256\": \"fake-hash-number3\"},\n                            \"url\": \"https://link3\",\n                        },\n                    ]\n                }\n            },\n            {\n                \"https://link1\": \"sha256:fake-hash-number1\",\n                \"https://link2\": \"sha256:fake-hash-number2\",\n            },\n            id=\"return only bdist_wheel and sdist hashes\",\n        ),\n        pytest.param(None, {}, id=\"not found project data\"),\n        pytest.param({}, {}, id=\"not found releases key\"),\n        pytest.param({\"releases\": {}}, {}, id=\"not found version\"),\n        pytest.param({\"releases\": {\"0.1\": [{}]}}, {}, id=\"not found digests\"),\n        pytest.param(\n            {\n                \"releases\": {\n                    \"0.1\": [\n                        {\"packagetype\": \"bdist_wheel\", \"digests\": {}, \"url\": \"link\"}\n                    ]\n                }\n            },\n            {},\n            id=\"digests are empty\",\n        ),\n        pytest.param(\n            {\n                \"releases\": {\n                    \"0.1\": [\n                        {\n                            \"packagetype\": \"bdist_wheel\",\n                            \"digests\": {\"md5\": \"fake-hash\"},\n                            \"url\": \"https://link\",\n                        }\n                    ]\n                }\n            },\n            {},\n            id=\"not found sha256 algo\",\n        ),\n    ),\n)\ndef test_get_hashes_from_pypi(from_line, tmpdir, project_data, expected_hashes):\n    \"\"\"\n    Test PyPIRepository._get_hashes_from_pypi() returns expected hashes or None.\n    \"\"\"\n\n    class MockPyPIRepository(PyPIRepository):\n        def _get_project(self, ireq):\n            return project_data\n\n    pypi_repository = MockPyPIRepository(\n        [\"--no-cache-dir\"], cache_dir=(tmpdir / \"pypi-repo-cache\")\n    )\n    ireq = from_line(\"fake-package==0.1\")\n\n    actual_hashes = pypi_repository._get_hashes_from_pypi(ireq)\n    assert actual_hashes == expected_hashes\n\n\ndef test_get_hashes_from_mixed(pip_conf, from_line, tmpdir):\n    \"\"\"\n    Test PyPIRepository.get_hashes() returns hashes from both PyPi and extra indexes/links\n    \"\"\"\n\n    package_name = \"small-fake-multi-arch\"\n    package_version = \"0.1\"\n\n    # One candidate from PyPi and the rest from find-links / extra indexes\n    extra_index_link1 = Link(\"https://extra-index-link1\")\n    extra_index_link2 = Link(\"https://extra-index-link2\")\n    pypi_link = Link(\"https://pypi-link\")\n\n    all_candidates = [\n        InstallationCandidate(package_name, package_version, extra_index_link1),\n        InstallationCandidate(package_name, package_version, extra_index_link2),\n        InstallationCandidate(package_name, package_version, pypi_link),\n    ]\n\n    # Extra indexes hashes so we don't spend time computing them\n    file_hashes = {\n        extra_index_link1: \"sha256:hash-link1\",\n        extra_index_link2: \"sha256:hash-link2\",\n    }\n    pypi_hash = \"pypi-hash\"\n\n    class MockPyPIRepository(PyPIRepository):\n        def _get_project(self, ireq):\n            return {\n                \"releases\": {\n                    package_version: [\n                        {\n                            \"packagetype\": \"bdist_wheel\",\n                            \"digests\": {\"sha256\": pypi_hash},\n                            \"url\": str(pypi_link),\n                        },\n                    ]\n                }\n            }\n\n        def find_all_candidates(self, req_name):\n            return all_candidates\n\n        def _get_file_hash(self, link):\n            return file_hashes[link]\n\n    pypi_repository = MockPyPIRepository(\n        [\"--no-cache-dir\"], cache_dir=(tmpdir / \"pypi-repo-cache\")\n    )\n\n    ireq = from_line(f\"{package_name}=={package_version}\")\n\n    expected_hashes = {\"sha256:\" + pypi_hash} | set(file_hashes.values())\n\n    actual_hashes = pypi_repository.get_hashes(ireq)\n    assert actual_hashes == expected_hashes\n\n\ndef test_get_project__returns_data(from_line, tmpdir, monkeypatch, pypi_repository):\n    \"\"\"\n    Test PyPIRepository._get_project() returns expected project data.\n    \"\"\"\n    expected_data = {\"releases\": {\"0.1\": [{\"digests\": {\"sha256\": \"fake-hash\"}}]}}\n\n    class MockResponse:\n        status_code = 200\n\n        @staticmethod\n        def json():\n            return expected_data\n\n    def mock_get(*args, **kwargs):\n        return MockResponse()\n\n    monkeypatch.setattr(pypi_repository.session, \"get\", mock_get)\n    ireq = from_line(\"fake-package==0.1\")\n\n    actual_data = pypi_repository._get_project(ireq)\n    assert actual_data == expected_data\n\n\ndef test_get_project__handles_http_error(\n    from_line, tmpdir, monkeypatch, pypi_repository\n):\n    \"\"\"\n    Test PyPIRepository._get_project() returns None if HTTP error is raised.\n    \"\"\"\n\n    def mock_get(*args, **kwargs):\n        raise HTTPError(\"test http error\")\n\n    monkeypatch.setattr(pypi_repository.session, \"get\", mock_get)\n    ireq = from_line(\"fake-package==0.1\")\n\n    actual_data = pypi_repository._get_project(ireq)\n    assert actual_data is None\n\n\ndef test_get_project__handles_json_decode_error(\n    from_line, tmpdir, monkeypatch, pypi_repository\n):\n    \"\"\"\n    Test PyPIRepository._get_project() returns None if JSON decode error is raised.\n    \"\"\"\n\n    class MockResponse:\n        status_code = 200\n\n        @staticmethod\n        def json():\n            raise ValueError(\"test json error\")\n\n    def mock_get(*args, **kwargs):\n        return MockResponse()\n\n    monkeypatch.setattr(pypi_repository.session, \"get\", mock_get)\n    ireq = from_line(\"fake-package==0.1\")\n\n    actual_data = pypi_repository._get_project(ireq)\n    assert actual_data is None\n\n\ndef test_get_project__handles_404(from_line, tmpdir, monkeypatch, pypi_repository):\n    \"\"\"\n    Test PyPIRepository._get_project() returns None if PyPI\n    response's status code is 404.\n    \"\"\"\n\n    class MockResponse:\n        status_code = 404\n\n    def mock_get(*args, **kwargs):\n        return MockResponse()\n\n    monkeypatch.setattr(pypi_repository.session, \"get\", mock_get)\n    ireq = from_line(\"fake-package==0.1\")\n\n    actual_data = pypi_repository._get_project(ireq)\n    assert actual_data is None\n\n\ndef test_name_collision(from_line, pypi_repository, make_package, make_sdist, tmpdir):\n    \"\"\"\n    Test to ensure we don't fail if there are multiple URL-based requirements\n    ending with the same filename where later ones depend on earlier, e.g.\n    https://git.example.com/requirement1/main.zip#egg=req_package_1\n    https://git.example.com/requirement2/main.zip#egg=req_package_2\n    In this case, if req_package_2 depends on req_package_1 we don't want to\n    fail due to issues such as caching the requirement based on filename.\n    \"\"\"\n    packages = {\n        \"test_package_1\": make_package(\"test_package_1\", version=\"0.1\"),\n        \"test_package_2\": make_package(\n            \"test_package_2\", version=\"0.1\", install_requires=[\"test-package-1\"]\n        ),\n    }\n\n    for pkg_name, pkg in packages.items():\n        pkg_path = tmpdir / pkg_name\n\n        make_sdist(pkg, pkg_path, \"--formats=zip\")\n\n        os.rename(\n            os.path.join(pkg_path, f\"{pkg_name}-0.1.zip\"),\n            os.path.join(pkg_path, \"main.zip\"),\n        )\n\n    name_collision_1 = \"file://{dist_path}#egg=test_package_1\".format(\n        dist_path=tmpdir / \"test_package_1\" / \"main.zip\"\n    )\n    ireq = from_line(name_collision_1)\n    deps = pypi_repository.get_dependencies(ireq)\n    assert len(deps) == 0\n\n    name_collision_2 = \"file://{dist_path}#egg=test_package_2\".format(\n        dist_path=tmpdir / \"test_package_2\" / \"main.zip\"\n    )\n    ireq = from_line(name_collision_2)\n    deps = pypi_repository.get_dependencies(ireq)\n    assert len(deps) == 1\n    assert deps.pop().name == \"test-package-1\"\n"
  },
  {
    "path": "tests/test_resolver.py",
    "content": "from __future__ import annotations\n\nimport pytest\nfrom pip._internal.exceptions import DistributionNotFound\nfrom pip._internal.utils.urls import path_to_url\n\nfrom piptools.exceptions import NoCandidateFound\nfrom piptools.resolver import RequirementSummary, combine_install_requirements\n\n\n@pytest.mark.parametrize(\n    (\"input\", \"expected\", \"prereleases\", \"unsafe_constraints\"),\n    (\n        (tup + (False, set())[len(tup) - 2 :])\n        for tup in [\n            ([\"Django\"], [\"django==1.8\"]),\n            (\n                [\"Flask\"],\n                [\n                    \"flask==0.10.1\",\n                    \"itsdangerous==0.24 (from flask==0.10.1)\",\n                    \"markupsafe==0.23 (from jinja2==2.7.3->flask==0.10.1)\",\n                    \"jinja2==2.7.3 (from flask==0.10.1)\",\n                    \"werkzeug==0.10.4 (from flask==0.10.1)\",\n                ],\n            ),\n            ([\"Jinja2\", \"markupsafe\"], [\"jinja2==2.7.3\", \"markupsafe==0.23\"]),\n            # We should return a normal release version if prereleases is False\n            ([\"SQLAlchemy\"], [\"sqlalchemy==0.9.9\"]),\n            # We should return the prerelease version if prereleases is True\n            ([\"SQLAlchemy\"], [\"sqlalchemy==1.0.0b5\"], True),\n            # Ipython has extras available, but we don't require them in this test\n            (\n                [\"ipython\"],\n                [\"ipython==2.1.0\", \"gnureadline==6.3.3 (from ipython==2.1.0)\"],\n            ),\n            # We should get dependencies for extras\n            (\n                [\"ipython[notebook]\"],\n                [\n                    \"ipython[notebook]==2.1.0\",\n                    \"pyzmq==2.1.12 (from ipython[notebook]==2.1.0)\",\n                    \"jinja2==2.7.3 (from ipython[notebook]==2.1.0)\",\n                    \"tornado==3.2.2 (from ipython[notebook]==2.1.0)\",\n                    \"markupsafe==0.23 (from jinja2==2.7.3->ipython[notebook]==2.1.0)\",\n                    \"gnureadline==6.3.3 (from ipython[notebook]==2.1.0)\",\n                ],\n            ),\n            # We should get dependencies for multiple extras\n            (\n                [\"ipython[notebook,nbconvert]\"],\n                [\n                    # Note that the extras should be sorted\n                    \"ipython[nbconvert,notebook]==2.1.0\",\n                    \"pyzmq==2.1.12 (from ipython[nbconvert,notebook]==2.1.0)\",\n                    \"jinja2==2.7.3 (from ipython[nbconvert,notebook]==2.1.0)\",\n                    \"tornado==3.2.2 (from ipython[nbconvert,notebook]==2.1.0)\",\n                    (\n                        \"markupsafe==0.23 \"\n                        \"(from jinja2==2.7.3->ipython[nbconvert,notebook]==2.1.0)\"\n                    ),\n                    \"gnureadline==6.3.3 (from ipython[nbconvert,notebook]==2.1.0)\",\n                    \"pygments==1.5 (from ipython[nbconvert,notebook]==2.1.0)\",\n                    \"sphinx==0.3 (from ipython[nbconvert,notebook]==2.1.0)\",\n                ],\n            ),\n            # We must take the union of all extras\n            (\n                [\"ipython[notebook]\", \"ipython[nbconvert]\"],\n                [\n                    # Note that the extras should be sorted\n                    \"ipython[nbconvert,notebook]==2.1.0\",\n                    \"pyzmq==2.1.12 (from ipython[nbconvert,notebook]==2.1.0)\",\n                    \"jinja2==2.7.3 (from ipython[nbconvert,notebook]==2.1.0)\",\n                    \"tornado==3.2.2 (from ipython[nbconvert,notebook]==2.1.0)\",\n                    (\n                        \"markupsafe==0.23 \"\n                        \"(from jinja2==2.7.3->ipython[nbconvert,notebook]==2.1.0)\"\n                    ),\n                    \"gnureadline==6.3.3 (from ipython[nbconvert,notebook]==2.1.0)\",\n                    \"pygments==1.5 (from ipython[nbconvert,notebook]==2.1.0)\",\n                    \"sphinx==0.3 (from ipython[nbconvert,notebook]==2.1.0)\",\n                ],\n            ),\n            # We must remove child dependencies from result if parent\n            # is removed (e.g. vine from amqp>=2.0)\n            # See: GH-370\n            # because of updated dependencies in the test index, we need to pin celery\n            # in order to reproduce vine removal (because it was added in later\n            # releases)\n            (\n                [\"celery<=3.1.23\", \"librabbitmq\"],\n                [\n                    \"amqp==1.4.9 (from librabbitmq==1.6.1)\",\n                    \"anyjson==0.3.3 (from kombu==3.0.35->celery==3.1.23)\",\n                    \"billiard==3.5.0.2 (from celery==3.1.23)\",\n                    \"celery==3.1.23\",\n                    \"kombu==3.0.35 (from celery==3.1.23)\",\n                    \"librabbitmq==1.6.1\",\n                    \"pytz==2016.4 (from celery==3.1.23)\",\n                ],\n            ),\n            # Support specifying loose top-level requirements that could also appear as\n            # pinned subdependencies.\n            (\n                [\"billiard\", \"celery\", \"fake-piptools-test-with-pinned-deps\"],\n                [\n                    \"amqp==1.4.9 (from kombu==3.0.35->celery==3.1.18)\",\n                    \"anyjson==0.3.3 (from kombu==3.0.35->celery==3.1.18)\",\n                    \"billiard==3.3.0.23\",\n                    \"celery==3.1.18\",  # this is pinned from test subdependency\n                    \"fake-piptools-test-with-pinned-deps==0.1\",\n                    \"kombu==3.0.35 (from celery==3.1.18)\",\n                    \"pytz==2016.4 (from celery==3.1.18)\",\n                ],\n            ),\n            # Exclude package dependency of setuptools as it is unsafe.\n            (\n                [\"html5lib\"],\n                [\"html5lib==0.999999999\"],\n                False,\n                {\"setuptools==35.0.0 (from html5lib==0.999999999)\"},\n            ),\n            # We shouldn't include irrelevant pip constraints\n            # See: GH-471\n            (\n                [\"Flask\", (\"click\", True), (\"itsdangerous\", True)],\n                [\n                    \"flask==0.10.1\",\n                    \"itsdangerous==0.24\",\n                    \"markupsafe==0.23 (from jinja2==2.7.3->flask==0.10.1)\",\n                    \"jinja2==2.7.3 (from flask==0.10.1)\",\n                    \"werkzeug==0.10.4 (from flask==0.10.1)\",\n                ],\n            ),\n            # We shouldn't fail on invalid irrelevant pip constraints\n            # See: GH-1178\n            (\n                [\"Flask\", (\"missing-dependency<1.0\", True), (\"itsdangerous\", True)],\n                [\n                    \"flask==0.10.1\",\n                    \"itsdangerous==0.24\",\n                    \"markupsafe==0.23 (from jinja2==2.7.3->flask==0.10.1)\",\n                    \"jinja2==2.7.3 (from flask==0.10.1)\",\n                    \"werkzeug==0.10.4 (from flask==0.10.1)\",\n                ],\n            ),\n            # Unsafe dependencies should be filtered\n            (\n                [\"setuptools==35.0.0\", \"anyjson==0.3.3\"],\n                [\"anyjson==0.3.3\"],\n                False,\n                {\"setuptools==35.0.0\"},\n            ),\n            (\n                [\"fake-piptools-test-with-unsafe-deps==0.1\"],\n                [\n                    \"appdirs==1.4.9 (from \"\n                    \"setuptools==34.0.0->fake-piptools-test-with-unsafe-deps==0.1)\",\n                    \"fake-piptools-test-with-unsafe-deps==0.1\",\n                    \"packaging==16.8 (from \"\n                    \"setuptools==34.0.0->fake-piptools-test-with-unsafe-deps==0.1)\",\n                ],\n                False,\n                {\n                    (\n                        \"setuptools==34.0.0 (from \"\n                        \"fake-piptools-test-with-unsafe-deps==0.1)\"\n                    ),\n                },\n            ),\n            # Git URL requirement\n            # See: GH-851\n            (\n                [\n                    \"git+https://github.com/celery/billiard#egg=billiard\",\n                    \"celery==4.0.2\",\n                ],\n                [\n                    \"amqp==2.1.4 (from kombu==4.0.2->celery==4.0.2)\",\n                    \"kombu==4.0.2 (from celery==4.0.2)\",\n                    \"billiard<3.6.0,>=3.5.0.2 from \"\n                    \"git+https://github.com/celery/billiard#egg=billiard\",\n                    \"vine==1.1.3 (from amqp==2.1.4->kombu==4.0.2->celery==4.0.2)\",\n                    \"celery==4.0.2\",\n                    \"pytz==2016.4 (from celery==4.0.2)\",\n                ],\n            ),\n            # Check that dependencies of relevant constraints are resolved\n            (\n                [\"aiohttp\", (\"yarl==1.4.2\", True)],\n                [\"aiohttp==3.6.2\", \"idna==2.8 (from yarl==1.4.2)\", \"yarl==1.4.2\"],\n            ),\n        ]\n    ),\n)\ndef test_resolver(\n    resolver, from_line, input, expected, prereleases, unsafe_constraints\n):\n    input = [line if isinstance(line, tuple) else (line, False) for line in input]\n    input = [from_line(req[0], constraint=req[1]) for req in input]\n    resolver = resolver(input, prereleases=prereleases)\n    output = resolver.resolve()\n    output = {str(line) for line in output}\n    assert output == {str(line) for line in expected}\n    assert {str(line) for line in resolver.unsafe_constraints} == unsafe_constraints\n\n\n@pytest.mark.parametrize(\n    (\"input\", \"expected\", \"prereleases\"),\n    (\n        (tup + (False,))[:3]\n        for tup in [\n            (\n                [\"setuptools==34.0.0\"],\n                [\n                    \"appdirs==1.4.9 (from setuptools==34.0.0)\",\n                    \"packaging==16.8 (from setuptools==34.0.0)\",\n                    \"setuptools==34.0.0\",\n                ],\n            ),\n            (\n                [\"fake-piptools-test-with-unsafe-deps==0.1\"],\n                [\n                    (\n                        \"appdirs==1.4.9 (from \"\n                        \"setuptools==34.0.0->fake-piptools-test-with-unsafe-deps==0.1)\"\n                    ),\n                    (\n                        \"setuptools==34.0.0 \"\n                        \"(from fake-piptools-test-with-unsafe-deps==0.1)\"\n                    ),\n                    (\n                        \"packaging==16.8 (from \"\n                        \"setuptools==34.0.0->fake-piptools-test-with-unsafe-deps==0.1)\"\n                    ),\n                    \"fake-piptools-test-with-unsafe-deps==0.1\",\n                ],\n            ),\n        ]\n    ),\n)\ndef test_resolver__allows_unsafe_deps(\n    resolver, from_line, input, expected, prereleases\n):\n    input = [line if isinstance(line, tuple) else (line, False) for line in input]\n    input = [from_line(req[0], constraint=req[1]) for req in input]\n    output = resolver(input, prereleases=prereleases, allow_unsafe=True).resolve()\n    output = {str(line) for line in output}\n    assert output == {str(line) for line in expected}\n\n\n@pytest.mark.parametrize(\n    (\n        \"input\",\n        \"expected\",\n        \"unsafe_packages\",\n        \"unsafe_constraints\",\n    ),\n    (\n        (\n            [\"fake-piptools-test-with-pinned-deps==0.1\"],\n            {\n                \"fake-piptools-test-with-pinned-deps==0.1\",\n                \"pytz==2016.4 (from celery==3.1.18->fake-piptools-test-with-pinned-deps==0.1)\",\n                \"billiard==3.3.0.23 (from \"\n                \"celery==3.1.18->fake-piptools-test-with-pinned-deps==0.1)\",\n                \"celery==3.1.18 (from fake-piptools-test-with-pinned-deps==0.1)\",\n                \"anyjson==0.3.3 (from \"\n                \"kombu==3.0.35->celery==3.1.18->fake-piptools-test-with-pinned-deps==0.1)\",\n                \"amqp==1.4.9 (from \"\n                \"kombu==3.0.35->celery==3.1.18->fake-piptools-test-with-pinned-deps==0.1)\",\n            },\n            {\"kombu\"},\n            {\n                \"kombu==3.0.35 (from celery==3.1.18->fake-piptools-test-with-pinned-deps==0.1)\",\n            },\n        ),\n    ),\n)\ndef test_resolver__custom_unsafe_deps(\n    resolver,\n    from_line,\n    input,\n    expected,\n    unsafe_packages,\n    unsafe_constraints,\n):\n    input = [line if isinstance(line, tuple) else (line, False) for line in input]\n    input = [from_line(req[0], constraint=req[1]) for req in input]\n    resolver = resolver(\n        input,\n        unsafe_packages=unsafe_packages,\n    )\n    output = resolver.resolve()\n    output = {str(line) for line in output}\n\n    assert output == expected\n    assert {str(line) for line in resolver.unsafe_constraints} == unsafe_constraints\n\n\ndef test_resolver__max_number_rounds_reached(resolver, from_line):\n    \"\"\"\n    Resolver should raise an exception if max round has been reached.\n    \"\"\"\n    input = [from_line(\"django\")]\n    with pytest.raises(RuntimeError, match=\"after 0 rounds of resolving\"):\n        resolver(input).resolve(max_rounds=0)\n\n\ndef test_iter_dependencies(resolver, from_line):\n    \"\"\"\n    Dependencies should be pinned or editable.\n    \"\"\"\n    ireq = from_line(\"django>=1.8\")\n    res = resolver([])\n\n    with pytest.raises(\n        TypeError, match=\"Expected pinned or editable requirement, got django>=1.8\"\n    ):\n        next(res._iter_dependencies(ireq))\n\n\ndef test_iter_dependencies_results(resolver, from_line):\n    res = resolver([])\n    ireq = from_line(\"aiohttp==3.6.2\")\n    assert next(res._iter_dependencies(ireq)).comes_from == ireq\n\n\ndef test_iter_dependencies_ignores_constraints(resolver, from_line):\n    res = resolver([])\n    ireq = from_line(\"aiohttp==3.6.2\", constraint=True)\n    with pytest.raises(StopIteration):\n        next(res._iter_dependencies(ireq))\n\n\ndef test_iter_dependencies_after_combine_install_requirements(\n    pypi_repository, base_resolver, make_package, from_line\n):\n    res = base_resolver([], repository=pypi_repository)\n\n    sub_deps = [\"click\"]\n    package_a = make_package(\"package-a\", install_requires=sub_deps)\n    package_b = make_package(\"package-b\", install_requires=[\"package-a\"])\n\n    local_package_a = from_line(path_to_url(package_a))\n    assert [dep.name for dep in res._iter_dependencies(local_package_a)] == sub_deps\n\n    package_a_from_b = from_line(\"package-a\", comes_from=path_to_url(package_b))\n    combined = combine_install_requirements([local_package_a, package_a_from_b])\n    assert [dep.name for dep in res._iter_dependencies(combined)] == sub_deps\n\n\ndef test_iter_dependencies_after_combine_install_requirements_extras(\n    pypi_repository, base_resolver, make_package, from_line\n):\n    res = base_resolver([], repository=pypi_repository)\n\n    package_a = make_package(\n        \"package-a\", extras_require={\"click\": [\"click\"], \"celery\": [\"celery\"]}\n    )\n    package_b = make_package(\"package-b\", install_requires=[\"package-a\"])\n\n    local_package_a = from_line(path_to_url(package_a))\n    assert [dep.name for dep in res._iter_dependencies(local_package_a)] == []\n\n    package_a_from_b = from_line(\"package-a[click]\", comes_from=path_to_url(package_b))\n    package_a_with_other_extra = from_line(\"package-a[celery]\")\n    combined = combine_install_requirements(\n        [local_package_a, package_a_from_b, package_a_with_other_extra]\n    )\n\n    dependency_names = {dep.name for dep in res._iter_dependencies(combined)}\n    assert {\"celery\", \"click\"}.issubset(dependency_names)\n\n\ndef test_combine_install_requirements(from_line):\n    celery30 = from_line(\"celery>3.0\", comes_from=\"-r requirements.in\")\n    celery31 = from_line(\"celery==3.1.1\", comes_from=from_line(\"fake-package\"))\n    celery32 = from_line(\"celery<3.2\")\n\n    combined = combine_install_requirements([celery30, celery31])\n    assert combined.comes_from == celery31.comes_from  # shortest string\n    assert set(combined._source_ireqs) == {celery30, celery31}\n    assert str(combined.req.specifier) == \"==3.1.1,>3.0\"\n\n    combined_all = combine_install_requirements([celery32, combined])\n    assert combined_all.comes_from is None\n    assert set(combined_all._source_ireqs) == {celery30, celery31, celery32}\n    assert str(combined_all.req.specifier) == \"<3.2,==3.1.1,>3.0\"\n\n\ndef _test_combine_install_requirements_extras(with_extra, without_extra):\n    combined = combine_install_requirements([without_extra, with_extra])\n    assert str(combined) == str(with_extra)\n    assert combined.extras == with_extra.extras\n\n    combined = combine_install_requirements([with_extra, without_extra])\n    assert str(combined) == str(with_extra)\n    assert combined.extras == with_extra.extras\n\n\ndef test_combine_install_requirements_extras_req(from_line, make_package):\n    \"\"\"\n    Extras should be unioned in combined install requirements\n    (whether or not InstallRequirement.req is None, and testing either order of the inputs)\n    \"\"\"\n    with_extra = from_line(\"edx-opaque-keys[django]==1.0.1\")\n    assert with_extra.req is not None\n    without_extra = from_line(\"edx-opaque-keys\")\n    assert without_extra.req is not None\n\n    _test_combine_install_requirements_extras(with_extra, without_extra)\n\n\ndef test_combine_install_requirements_extras_no_req(from_line, make_package):\n    \"\"\"\n    Extras should be unioned in combined install requirements\n    (whether or not InstallRequirement.req is None, and testing either order of the inputs)\n    \"\"\"\n    test_package = make_package(\"test-package\", extras_require={\"extra\": []})\n    local_package_with_extra = from_line(f\"{test_package}[extra]\")\n    assert local_package_with_extra.req is None\n    local_package_without_extra = from_line(path_to_url(test_package))\n    assert local_package_without_extra.req is None\n\n    _test_combine_install_requirements_extras(\n        local_package_with_extra, local_package_without_extra\n    )\n\n\ndef test_combine_install_requirements_with_paths(from_line, make_package):\n    name = \"fake_package_b\"\n    version = \"1.0.0\"\n\n    test_package = make_package(name, version=version)\n    fake_package = from_line(f\"{name} @ {path_to_url(test_package)}\")\n    fake_package_name = from_line(f\"{name}=={version}\", comes_from=from_line(name))\n\n    for pair in [(fake_package, fake_package_name), (fake_package_name, fake_package)]:\n        combined = combine_install_requirements(pair)\n        assert str(combined.specifier) == str(fake_package_name.specifier)\n        assert str(combined.link) == str(fake_package.link)\n        assert str(combined.local_file_path) == str(fake_package.local_file_path)\n        assert str(combined.original_link) == str(fake_package.original_link)\n\n\ndef test_combine_install_requirements_for_one_package_with_multiple_extras(\n    from_line,\n):\n    \"\"\"Regression test for https://github.com/jazzband/pip-tools/pull/1512\"\"\"\n    pkg1 = from_line(\"ray[default]==1.1.1\")\n    pkg2 = from_line(\"ray[tune]==1.1.1\")\n    combined = combine_install_requirements([pkg1, pkg2])\n\n    assert str(combined) == \"ray[default,tune]==1.1.1\"\n\n\ndef test_compile_failure_shows_provenance(resolver, from_line):\n    \"\"\"\n    Provenance of conflicting dependencies should be printed on failure.\n    \"\"\"\n    requirements = [\n        from_line(\"fake-piptools-test-with-pinned-deps==0.1\"),\n        from_line(\"celery>3.2\"),\n    ]\n\n    with pytest.raises(NoCandidateFound) as err:\n        resolver(requirements).resolve()\n    lines = str(err.value).splitlines()\n    assert lines[-2].strip() == \"celery>3.2\"\n    assert (\n        lines[-1].strip()\n        == \"celery==3.1.18 (from fake-piptools-test-with-pinned-deps==0.1)\"\n    )\n\n\n@pytest.mark.parametrize(\n    (\"left_hand\", \"right_hand\", \"expected\"),\n    (\n        (\"test_package\", \"test_package\", True),\n        (\"test_package==1.2.3\", \"test_package==1.2.3\", True),\n        (\"test_package>=1.2.3\", \"test_package>=1.2.3\", True),\n        (\"test_package==1.2\", \"test_package==1.2.0\", True),\n        (\"test_package>=1.2\", \"test_package>=1.2.0\", True),\n        (\"test_package[foo,bar]==1.2\", \"test_package[bar,foo]==1.2\", True),\n        (\"test_package[foo,bar]>=1.2\", \"test_package[bar,foo]>=1.2\", True),\n        (\"test_package[foo,bar]==1.2\", \"test_package[bar,foo]==1.2.0\", True),\n        (\"test_package[foo,bar]>=1.2\", \"test_package[bar,foo]>=1.2.0\", True),\n        (\"test_package\", \"other_test_package\", False),\n        (\"test_package==1.2.3\", \"other_test_package==1.2.3\", False),\n        (\"test_package==1.2.3\", \"test_package==1.2.4\", False),\n        (\"test_package>=1.2.3\", \"test_package>=1.2.4\", False),\n        (\"test_package>=1.2.3\", \"test_package<=1.2.3\", False),\n        (\"test_package==1.2\", \"test_package==1.2.3\", False),\n        (\"test_package>=1.2\", \"test_package>=1.2.3\", False),\n        (\"test_package[foo]==1.2\", \"test_package[bar]==1.2.0\", False),\n        (\"test_package[foo]>=1.2\", \"test_package[bar]>=1.2.0\", False),\n        (\"test_package[foo,bar]>=1.2\", \"test_package[bar]>=1.2.0\", False),\n        (\"test_package[foo,bar]>=1.2\", \"test_package[bar,zee]>=1.2.0\", False),\n    ),\n)\ndef test_RequirementSummary_equality(from_line, left_hand, right_hand, expected):\n    \"\"\"\n    RequirementSummary should report proper equality.\n    \"\"\"\n    lh_summary = RequirementSummary(from_line(left_hand))\n    rh_summary = RequirementSummary(from_line(right_hand))\n    assert (lh_summary == rh_summary) is expected\n\n\n@pytest.mark.parametrize(\n    (\"left_hand\", \"right_hand\", \"expected\"),\n    (\n        (\"test_package\", \"test_package\", True),\n        (\"test_package==1.2.3\", \"test_package==1.2.3\", True),\n        (\"test_package>=1.2.3\", \"test_package>=1.2.3\", True),\n        (\"test_package==1.2\", \"test_package==1.2.0\", True),\n        (\"test_package>=1.2\", \"test_package>=1.2.0\", True),\n        (\"test_package[foo,bar]==1.2\", \"test_package[bar,foo]==1.2\", True),\n        (\"test_package[foo,bar]>=1.2\", \"test_package[bar,foo]>=1.2\", True),\n        (\"test_package[foo,bar]==1.2\", \"test_package[bar,foo]==1.2.0\", True),\n        (\"test_package[foo,bar]>=1.2\", \"test_package[bar,foo]>=1.2.0\", True),\n        (\"test_package\", \"other_test_package\", False),\n        (\"test_package==1.2.3\", \"other_test_package==1.2.3\", False),\n        (\"test_package==1.2.3\", \"test_package==1.2.4\", False),\n        (\"test_package>=1.2.3\", \"test_package>=1.2.4\", False),\n        (\"test_package>=1.2.3\", \"test_package<=1.2.3\", False),\n        (\"test_package==1.2\", \"test_package==1.2.3\", False),\n        (\"test_package>=1.2\", \"test_package>=1.2.3\", False),\n        (\"test_package[foo]==1.2\", \"test_package[bar]==1.2.0\", False),\n        (\"test_package[foo]>=1.2\", \"test_package[bar]>=1.2.0\", False),\n        (\"test_package[foo,bar]>=1.2\", \"test_package[bar]>=1.2.0\", False),\n        (\"test_package[foo,bar]>=1.2\", \"test_package[bar,zee]>=1.2.0\", False),\n    ),\n)\ndef test_RequirementSummary_hash_equality(from_line, left_hand, right_hand, expected):\n    \"\"\"\n    RequirementSummary hash for equivalent requirements should be equal.\n    \"\"\"\n    lh_summary = RequirementSummary(from_line(left_hand))\n    rh_summary = RequirementSummary(from_line(right_hand))\n    assert (hash(lh_summary) == hash(rh_summary)) is expected\n\n\ndef test_requirement_summary_with_other_objects(from_line):\n    \"\"\"\n    RequirementSummary should not be equal to any other object\n    \"\"\"\n    requirement_summary = RequirementSummary(from_line(\"test_package==1.2.3\"))\n    other_object = object()\n    assert requirement_summary != other_object\n\n\n@pytest.mark.parametrize(\n    (\"exception\", \"cause\"),\n    (\n        pytest.param(DistributionNotFound, None, id=\"without cause\"),\n        pytest.param(DistributionNotFound, ZeroDivisionError, id=\"with cause\"),\n    ),\n)\ndef test_catch_distribution_not_found_error(backtracking_resolver, exception, cause):\n    \"\"\"\n    Test internal edge-cases when backtracking resolver catches\n    and re-raises ``DistributionNotFound`` error with/without causes.\n    \"\"\"\n    resolver = backtracking_resolver([])\n\n    class FakePipResolver:\n        def resolve(self, *args, **kwargs):\n            raise exception from cause\n\n    with pytest.raises(DistributionNotFound):\n        resolver._do_resolve(\n            resolver=FakePipResolver(),\n            compatible_existing_constraints={},\n        )\n"
  },
  {
    "path": "tests/test_sync.py",
    "content": "from __future__ import annotations\n\nimport io\nimport os\nimport sys\nimport tempfile\nfrom collections import Counter\nfrom unittest import mock\n\nimport pytest\nfrom pip._internal.utils.urls import path_to_url\n\nfrom piptools.exceptions import IncompatibleRequirements\nfrom piptools.sync import dependency_tree, diff, merge, sync\n\nfrom .constants import PACKAGES_PATH\n\n\n@pytest.fixture\ndef mocked_tmp_file():\n    with mock.patch.object(tempfile, \"NamedTemporaryFile\") as m:\n        yield m.return_value\n\n\n@pytest.fixture\ndef mocked_tmp_req_file(mocked_tmp_file):\n    with mock.patch(\"os.unlink\"):\n        mocked_tmp_file.name = \"requirements.txt\"\n        yield mocked_tmp_file\n\n\n@pytest.mark.parametrize(\n    (\"installed\", \"root\", \"expected\"),\n    (\n        ([], \"pip-tools\", []),\n        ([(\"pip-tools==1\", [])], \"pip-tools\", [\"pip-tools\"]),\n        ([(\"pip-tools==1\", []), (\"django==1.7\", [])], \"pip-tools\", [\"pip-tools\"]),\n        (\n            [(\"pip-tools==1\", [\"click>=2\"]), (\"django==1.7\", []), (\"click==3\", [])],\n            \"pip-tools\",\n            [\"pip-tools\", \"click\"],\n        ),\n        (\n            [(\"pip-tools==1\", [\"click>=2\"]), (\"django==1.7\", []), (\"click==1\", [])],\n            \"pip-tools\",\n            [\"pip-tools\"],\n        ),\n        (\n            [(\"root==1\", [\"child==2\"]), (\"child==2\", [\"grandchild==3\"])],\n            \"root\",\n            [\"root\", \"child\"],\n        ),\n        (\n            [\n                (\"root==1\", [\"child==2\"]),\n                (\"child==2\", [\"grandchild==3\"]),\n                (\"grandchild==3\", []),\n            ],\n            \"root\",\n            [\"root\", \"child\", \"grandchild\"],\n        ),\n        (\n            [(\"root==1\", [\"child==2\"]), (\"child==2\", [\"root==1\"])],\n            \"root\",\n            [\"root\", \"child\"],\n        ),\n    ),\n)\ndef test_dependency_tree(fake_dist, installed, root, expected):\n    installed = {\n        distribution.key: distribution\n        for distribution in (fake_dist(name, deps) for name, deps in installed)\n    }\n\n    actual = dependency_tree(installed, root)\n    assert actual == set(expected)\n\n\ndef test_merge_detect_conflicts(from_line):\n    requirements = [from_line(\"flask==1\"), from_line(\"flask==2\")]\n\n    with pytest.raises(IncompatibleRequirements):\n        merge(requirements, ignore_conflicts=False)\n\n\ndef test_merge_ignore_conflicts(from_line):\n    requirements = [from_line(\"flask==1\"), from_line(\"flask==2\")]\n\n    assert Counter(requirements[1:2]) == Counter(\n        merge(requirements, ignore_conflicts=True)\n    )\n\n\ndef test_merge(from_line):\n    requirements = [\n        from_line(\"flask==1\"),\n        from_line(\"flask==1\"),\n        from_line(\"django==2\"),\n    ]\n\n    assert Counter(requirements[1:3]) == Counter(\n        merge(requirements, ignore_conflicts=False)\n    )\n\n\ndef test_merge_urls(from_line):\n    requirements = [\n        from_line(\"file:///example.zip#egg=example\"),\n        from_line(\"example\"),\n        from_line(\"file:///unrelated.zip\"),\n    ]\n\n    assert Counter(requirements[1:]) == Counter(\n        merge(requirements, ignore_conflicts=False)\n    )\n\n\n@pytest.mark.parametrize(\n    \"install_req\",\n    (\n        \"from_line\",\n        \"from_editable\",\n    ),\n)\ndef test_merge_no_name_urls(install_req, request):\n    install_req = request.getfixturevalue(install_req)\n    url = \"file:///example.zip\"\n    requirements = [\n        install_req(url),\n        install_req(url),\n    ]\n\n    assert Counter(requirements[1:]) == Counter(\n        merge(requirements, ignore_conflicts=False)\n    )\n\n\ndef test_diff_should_do_nothing():\n    installed = []  # empty env\n    reqs = []  # no requirements\n\n    to_install, to_uninstall = diff(reqs, installed)\n    assert to_install == set()\n    assert to_uninstall == set()\n\n\ndef test_diff_should_install(from_line):\n    installed = []  # empty env\n    reqs = [from_line(\"django==1.8\")]\n\n    to_install, to_uninstall = diff(reqs, installed)\n    assert {str(x.req) for x in to_install} == {\"django==1.8\"}\n    assert to_uninstall == set()\n\n\ndef test_diff_should_uninstall(fake_dist):\n    installed = [fake_dist(\"django==1.8\")]\n    reqs = []\n\n    to_install, to_uninstall = diff(reqs, installed)\n    assert to_install == set()\n    assert to_uninstall == {\"django\"}  # no version spec when uninstalling\n\n\ndef test_diff_should_not_uninstall(fake_dist):\n    ignored = (\n        \"pip==7.1.0\",\n        \"pip-tools==1.1.1\",\n        \"pip-review==1.1.1\",\n        \"pkg-resources==0.0.0\",\n        \"python==3.0\",\n        \"wsgiref==0.1\",\n        \"argparse==0.1\",\n    )\n    # on Python 3.12 and above, `ensurepip` and `venv` do not default to installing\n    # 'setuptools' -- as such, `pip` changes behavior on many Python 3.12 environments\n    # to use isolated builds\n    if sys.version_info < (3, 12):\n        ignored += (\n            \"setuptools==34.0.0\",\n            \"wheel==0.29.0\",\n            \"distribute==0.1\",\n        )\n    installed = [fake_dist(pkg) for pkg in ignored]\n    reqs = []\n\n    to_uninstall = diff(reqs, installed)[1]\n    assert to_uninstall == set()\n\n\ndef test_diff_should_update(fake_dist, from_line):\n    installed = [fake_dist(\"django==1.7\")]\n    reqs = [from_line(\"django==1.8\")]\n\n    to_install, to_uninstall = diff(reqs, installed)\n    assert {str(x.req) for x in to_install} == {\"django==1.8\"}\n    assert to_uninstall == set()\n\n\ndef test_diff_should_install_with_markers(from_line):\n    installed = []\n    reqs = [from_line(\"subprocess32==3.2.7 ; python_version=='2.7'\")]\n\n    to_install, to_uninstall = diff(reqs, installed)\n    assert to_install == set()\n    assert to_uninstall == set()\n\n\ndef test_diff_should_uninstall_with_markers(fake_dist, from_line):\n    installed = [fake_dist(\"subprocess32==3.2.7\")]\n    reqs = [from_line(\"subprocess32==3.2.7 ; python_version=='2.7'\")]\n\n    to_install, to_uninstall = diff(reqs, installed)\n    assert to_install == set()\n    assert to_uninstall == {\"subprocess32\"}\n\n\ndef test_diff_leave_packaging_packages_alone(fake_dist, from_line):\n    # Suppose an env contains Django, and pip itself\n    installed = [\n        fake_dist(\"django==1.7\"),\n        fake_dist(\"first==2.0.1\"),\n        fake_dist(\"pip==7.1.0\"),\n    ]\n\n    # Then this Django-only requirement should keep pip around (i.e. NOT\n    # uninstall it), but uninstall first\n    reqs = [from_line(\"django==1.7\")]\n\n    to_install, to_uninstall = diff(reqs, installed)\n    assert to_install == set()\n    assert to_uninstall == {\"first\"}\n\n\ndef test_diff_leave_piptools_and_its_dependencies_alone(fake_dist, from_line):\n    # Suppose an env contains Django, and pip-tools itself (including all of\n    # its dependencies)\n    installed = [\n        fake_dist(\"django==1.7\"),\n        fake_dist(\"first==2.0.1\"),\n        fake_dist(\"pip-tools==1.1.1\", [\"click>=4\", \"first\", \"six\", \"build\"]),\n        fake_dist(\"six==1.9.0\"),\n        fake_dist(\"click==4.1\"),\n        fake_dist(\"foobar==0.3.6\"),\n        fake_dist(\"build==0.10.0\", [\"pyproject_hooks\"]),\n        fake_dist(\"pyproject_hooks==1.0.0\"),\n    ]\n\n    # Then this Django-only requirement should keep pip around (i.e. NOT\n    # uninstall it), but uninstall first\n    reqs = [from_line(\"django==1.7\")]\n\n    to_install, to_uninstall = diff(reqs, installed)\n    assert to_install == set()\n    assert to_uninstall == {\"foobar\"}\n\n\ndef test_diff_with_editable(fake_dist, from_editable):\n    installed = [fake_dist(\"small-fake-with-deps==0.0.1\"), fake_dist(\"six==1.10.0\")]\n    path_to_package = os.path.join(PACKAGES_PATH, \"small_fake_with_deps\")\n    reqs = [from_editable(path_to_package)]\n    to_install, to_uninstall = diff(reqs, installed)\n\n    # FIXME: The editable package is uninstalled and reinstalled, including\n    # all its dependencies, even if the version numbers match.\n    assert to_uninstall == {\"six\", \"small-fake-with-deps\"}\n\n    assert len(to_install) == 1\n    package = list(to_install)[0]\n    assert package.editable\n    assert package.link.url == path_to_url(path_to_package)\n\n\ndef test_diff_with_matching_url_hash(fake_dist, from_line):\n    # if URL version is explicitly provided, use it to avoid reinstalling\n    line = \"example@file:///example.zip#sha1=abc\"\n    installed = [fake_dist(line)]\n    reqs = [from_line(line)]\n\n    to_install, to_uninstall = diff(reqs, installed)\n    assert to_install == set()\n    assert to_uninstall == set()\n\n\n@pytest.mark.parametrize(\n    (\"installed_dist\", \"compiled_req\"),\n    (\n        pytest.param(\"Django==1.7\", \"django==1.7\", id=\"case insensitive\"),\n        pytest.param(\n            \"jaraco.classes==3.2\",\n            \"jaraco-classes==3.2\",\n            id=\"different namespace notation\",\n        ),\n    ),\n)\ndef test_diff_respects_canonical_package_names(\n    fake_dist, from_line, installed_dist, compiled_req\n):\n    installed = [fake_dist(installed_dist)]\n    reqs = [from_line(compiled_req)]\n    to_install, to_uninstall = diff(reqs, installed)\n    assert to_install == set()\n    assert to_uninstall == set()\n\n\ndef test_diff_with_no_url_hash(fake_dist, from_line):\n    # if URL hash is not provided, assume the contents have\n    # changed and reinstall\n    line = \"example@file:///example.zip\"\n    installed = [fake_dist(line)]\n    reqs = [from_line(line)]\n\n    to_install, to_uninstall = diff(reqs, installed)\n    assert to_install == set(reqs)\n    assert to_uninstall == {\"example\"}\n\n\ndef test_diff_with_unequal_url_hash(fake_dist, from_line):\n    # if URL hashes mismatch, assume the contents have\n    # changed and reinstall\n    line = \"example@file:///example.zip#\"\n    installed = [fake_dist(line + \"sha1=abc\")]\n    reqs = [from_line(line + \"sha1=def\")]\n\n    to_install, to_uninstall = diff(reqs, installed)\n    assert to_install == set(reqs)\n    assert to_uninstall == {\"example @ file:///example.zip#sha1=abc\"}\n\n\ndef test_sync_install_temporary_requirement_file(\n    from_line, from_editable, mocked_tmp_req_file\n):\n    with mock.patch(\"piptools.sync.run\") as run:\n        to_install = {from_line(\"django==1.8\")}\n        sync(to_install, set())\n        run.assert_called_once_with(\n            [sys.executable, \"-m\", \"pip\", \"install\", \"-r\", mocked_tmp_req_file.name],\n            check=True,\n        )\n\n\ndef test_temporary_requirement_file_deleted(from_line, from_editable, mocked_tmp_file):\n    with mock.patch(\"piptools.sync.run\"):\n        to_install = {from_line(\"django==1.8\")}\n\n        with mock.patch(\"os.unlink\") as unlink:\n            sync(to_install, set())\n\n            unlink.assert_called_once_with(mocked_tmp_file.name)\n\n\ndef test_sync_requirement_file(from_line, from_editable, mocked_tmp_req_file):\n    with mock.patch(\"piptools.sync.run\"):\n        to_install = {\n            from_line(\"django==1.8\"),\n            from_editable(\"git+git://fake.org/x/y.git#egg=y\"),\n            from_line(\"click==4.0\"),\n            from_editable(\"git+git://fake.org/i/j.git#egg=j\"),\n            from_line(\"pytz==2017.2\"),\n        }\n\n        sync(to_install, set())\n\n        expected = (\n            \"click==4.0\\n\"\n            \"django==1.8\\n\"\n            \"-e git+git://fake.org/i/j.git#egg=j\\n\"\n            \"pytz==2017.2\\n\"\n            \"-e git+git://fake.org/x/y.git#egg=y\"\n        )\n        mocked_tmp_req_file.write.assert_called_once_with(expected)\n\n\ndef test_sync_requirement_file_with_hashes(\n    from_line, from_editable, mocked_tmp_req_file\n):\n    with mock.patch(\"piptools.sync.run\"):\n        to_install = {\n            from_line(\n                \"django==1.8\",\n                hash_options={\n                    \"sha256\": [\n                        \"6a03ce2feafdd193a0ba8a26dbd9773e\"\n                        \"757d2e5d5e7933a62eac129813bd381a\"\n                    ]\n                },\n            ),\n            from_line(\n                \"click==4.0\",\n                hash_options={\n                    \"sha256\": [\n                        \"9ab1d313f99b209f8f71a629f3683303\"\n                        \"0c8d7c72282cf7756834baf567dca662\"\n                    ]\n                },\n            ),\n            from_line(\n                \"pytz==2017.2\",\n                hash_options={\n                    \"sha256\": [\n                        \"d1d6729c85acea542367138286862712\"\n                        \"9432fba9a89ecbb248d8d1c7a9f01c67\",\n                        \"f5c056e8f62d45ba8215e5cb8f50dfcc\"\n                        \"b198b4b9fbea8500674f3443e4689589\",\n                    ]\n                },\n            ),\n        }\n\n        sync(to_install, set())\n\n        expected = (\n            \"click==4.0 \\\\\\n\"\n            \"    --hash=sha256:9ab1d313f99b209f8f71a629\"\n            \"f36833030c8d7c72282cf7756834baf567dca662\\n\"\n            \"django==1.8 \\\\\\n\"\n            \"    --hash=sha256:6a03ce2feafdd193a0ba8a26\"\n            \"dbd9773e757d2e5d5e7933a62eac129813bd381a\\n\"\n            \"pytz==2017.2 \\\\\\n\"\n            \"    --hash=sha256:d1d6729c85acea542367138286\"\n            \"8627129432fba9a89ecbb248d8d1c7a9f01c67 \\\\\\n\"\n            \"    --hash=sha256:f5c056e8f62d45ba8215e5cb8f\"\n            \"50dfccb198b4b9fbea8500674f3443e4689589\"\n        )\n        mocked_tmp_req_file.write.assert_called_once_with(expected)\n\n\ndef test_sync_up_to_date(capsys, runner):\n    \"\"\"\n    Everything up-to-date should be printed.\n    \"\"\"\n    sync(set(), set())\n    captured = capsys.readouterr()\n    assert captured.out.splitlines() == [\"Everything up-to-date\"]\n    assert captured.err == \"\"\n\n\n@mock.patch(\"piptools.sync.run\")\ndef test_sync_verbose(run, from_line):\n    \"\"\"\n    The -q option has to be passed to every pip calls.\n    \"\"\"\n    sync({from_line(\"django==1.8\")}, {from_line(\"click==4.0\")})\n    assert run.call_count == 2\n    for call in run.call_args_list:\n        run_args = call[0][0]\n        assert \"-q\" not in run_args\n\n\n@pytest.mark.parametrize(\n    (\"to_install\", \"to_uninstall\", \"expected_message\"),\n    (\n        ({\"django==1.8\", \"click==4.0\"}, set(), \"Would install:\"),\n        (set(), {\"django==1.8\", \"click==4.0\"}, \"Would uninstall:\"),\n    ),\n)\ndef test_sync_dry_run(\n    capsys, runner, from_line, to_install, to_uninstall, expected_message\n):\n    \"\"\"\n    Sync with --dry-run option prints what's is going to be installed/uninstalled.\n    \"\"\"\n    to_install = {from_line(pkg) for pkg in to_install}\n    sync(to_install, to_uninstall, dry_run=True)\n    captured = capsys.readouterr()\n    assert captured.out.splitlines() == [\n        expected_message,\n        \"  click==4.0\",\n        \"  django==1.8\",\n    ]\n    assert captured.err == \"\"\n\n\n@pytest.mark.parametrize(\n    (\"to_install\", \"to_uninstall\", \"expected_message\"),\n    (\n        ({\"django==1.8\", \"click==4.0\"}, set(), \"Would install:\"),\n        (set(), {\"django==1.8\", \"click==4.0\"}, \"Would uninstall:\"),\n    ),\n)\n@mock.patch(\"piptools.sync.run\")\ndef test_sync_ask_declined(\n    run, monkeypatch, capsys, from_line, to_install, to_uninstall, expected_message\n):\n    \"\"\"\n    Sync with --ask option does a dry run if the user declines\n    \"\"\"\n\n    monkeypatch.setattr(\"sys.stdin\", io.StringIO(\"n\\n\"))\n    to_install = {from_line(pkg) for pkg in to_install}\n    sync(to_install, to_uninstall, ask=True)\n\n    captured = capsys.readouterr()\n    assert captured.out.splitlines() == [\n        expected_message,\n        \"  click==4.0\",\n        \"  django==1.8\",\n        \"Would you like to proceed with these changes? [y/N]: \",\n    ]\n    assert captured.err == \"\"\n    run.assert_not_called()\n\n\n@pytest.mark.parametrize(\"dry_run\", (True, False))\n@mock.patch(\"piptools.sync.run\")\ndef test_sync_ask_accepted(run, monkeypatch, capsys, from_line, dry_run):\n    \"\"\"\n    pip should be called as normal when the user confirms, even with dry_run\n    \"\"\"\n    monkeypatch.setattr(\"sys.stdin\", io.StringIO(\"y\\n\"))\n    sync(\n        {from_line(\"django==1.8\")},\n        {from_line(\"click==4.0\")},\n        ask=True,\n        dry_run=dry_run,\n    )\n\n    assert run.call_count == 2\n    captured = capsys.readouterr()\n    assert captured.out.splitlines() == [\n        \"Would uninstall:\",\n        \"  click==4.0\",\n        \"Would install:\",\n        \"  django==1.8\",\n        \"Would you like to proceed with these changes? [y/N]: \",\n    ]\n    assert captured.err == \"\"\n\n\n@mock.patch(\"piptools.sync.run\")\ndef test_sync_uninstall_pip_command(run):\n    to_uninstall = [\"six\", \"django\", \"pytz\", \"click\"]\n\n    sync(set(), to_uninstall)\n    run.assert_called_once_with(\n        [sys.executable, \"-m\", \"pip\", \"uninstall\", \"-y\", *sorted(to_uninstall)],\n        check=True,\n    )\n"
  },
  {
    "path": "tests/test_top_level_editable.py",
    "content": ""
  },
  {
    "path": "tests/test_utils.py",
    "content": "from __future__ import annotations\n\nimport logging\nimport operator\nimport os\nimport shlex\nimport sys\nimport typing as _t\nfrom pathlib import Path\nfrom textwrap import dedent\n\nimport pytest\nfrom click import BadOptionUsage, Context, FileError\nfrom pip._internal.req import InstallRequirement\nfrom pip._internal.resolution.resolvelib.requirements import SpecifierRequirement\n\nfrom piptools.scripts.compile import cli as compile_cli\nfrom piptools.utils import (\n    as_tuple,\n    dedup,\n    drop_extras,\n    flat_map,\n    format_requirement,\n    format_specifier,\n    get_cli_options,\n    get_compile_command,\n    get_hashes_from_ireq,\n    get_sys_path_for_python_executable,\n    is_pinned_requirement,\n    is_url_requirement,\n    key_from_ireq,\n    key_from_req,\n    lookup_table,\n    lookup_table_from_tuples,\n    override_defaults_from_config_file,\n    select_config_file,\n)\n\n\ndef test_format_requirement(from_line):\n    ireq = from_line(\"test==1.2\")\n    assert format_requirement(ireq) == \"test==1.2\"\n\n\n@pytest.mark.parametrize(\n    (\"line\", \"expected\"),\n    (\n        pytest.param(\n            \"https://example.com/example.zip\",\n            \"https://example.com/example.zip\",\n            id=\"simple url\",\n        ),\n        pytest.param(\n            \"example @ https://example.com/example.zip\",\n            \"example @ https://example.com/example.zip\",\n            id=\"direct reference\",\n        ),\n        pytest.param(\n            \"Example @ https://example.com/example.zip\",\n            \"example @ https://example.com/example.zip\",\n            id=\"direct reference lowered case\",\n        ),\n        pytest.param(\n            \"example @ https://example.com/example.zip#egg=example\",\n            \"example @ https://example.com/example.zip\",\n            id=\"direct reference with egg in fragment\",\n        ),\n        pytest.param(\n            \"example @ https://example.com/example.zip#subdirectory=test&egg=example\",\n            \"example @ https://example.com/example.zip#subdirectory=test\",\n            id=\"direct reference with subdirectory and egg in fragment\",\n        ),\n        pytest.param(\n            \"example @ https://example.com/example.zip#subdirectory=test\"\n            \"&egg=example&sha1=594b7dd32bec37d8bf70a6ffa8866d30e93f3c42\",\n            \"example @ https://example.com/example.zip#subdirectory=test\"\n            \"&sha1=594b7dd32bec37d8bf70a6ffa8866d30e93f3c42\",\n            id=\"direct reference with subdirectory, hash and egg in fragment\",\n        ),\n        pytest.param(\n            \"example @ https://example.com/example.zip?egg=test\",\n            \"example @ https://example.com/example.zip?egg=test\",\n            id=\"direct reference with egg in query\",\n        ),\n        pytest.param(\n            \"example[b,c,a] @ https://example.com/example.zip\",\n            \"example[a,b,c] @ https://example.com/example.zip\",\n            id=\"direct reference with optional dependency\",\n        ),\n        pytest.param(\n            \"file:./vendor/package.zip\",\n            \"file:./vendor/package.zip\",\n            id=\"file scheme relative path\",\n        ),\n        pytest.param(\n            \"file:vendor/package.zip\",\n            \"file:vendor/package.zip\",\n            id=\"file scheme relative path\",\n        ),\n        pytest.param(\n            \"file:vendor/package.zip#egg=example\",\n            \"file:vendor/package.zip#egg=example\",\n            id=\"file scheme relative path with egg\",\n        ),\n        pytest.param(\n            \"file:./vendor/package.zip#egg=example\",\n            \"file:./vendor/package.zip#egg=example\",\n            id=\"file scheme relative path with egg\",\n        ),\n        pytest.param(\n            \"file:///vendor/package.zip\",\n            \"file:///vendor/package.zip\",\n            id=\"file scheme absolute path without direct reference\",\n        ),\n        pytest.param(\n            \"file:///vendor/package.zip#egg=test\",\n            \"test @ file:///vendor/package.zip\",\n            id=\"file scheme absolute path with egg\",\n        ),\n        pytest.param(\n            \"package @ file:///vendor/package.zip\",\n            \"package @ file:///vendor/package.zip\",\n            id=\"file scheme absolute path with direct reference\",\n        ),\n        pytest.param(\n            \"package @ file:///vendor/package.zip#egg=example\",\n            \"package @ file:///vendor/package.zip\",\n            id=\"file scheme absolute path with direct reference and egg\",\n        ),\n        pytest.param(\n            \"package @ file:///vendor/package.zip#egg=example&subdirectory=test\"\n            \"&sha1=594b7dd32bec37d8bf70a6ffa8866d30e93f3c42\",\n            \"package @ file:///vendor/package.zip#subdirectory=test\"\n            \"&sha1=594b7dd32bec37d8bf70a6ffa8866d30e93f3c42\",\n            id=\"full path with direct reference, egg, subdirectory and hash\",\n        ),\n    ),\n)\ndef test_format_requirement_url(from_line, line, expected):\n    ireq = from_line(line)\n    assert format_requirement(ireq) == expected\n\n\ndef test_format_requirement_editable_vcs(from_editable):\n    ireq = from_editable(\"git+git://fake.org/x/y.git#egg=y\")\n    assert format_requirement(ireq) == \"-e git+git://fake.org/x/y.git#egg=y\"\n\n\ndef test_format_requirement_editable_vcs_with_password(from_editable):\n    ireq = from_editable(\"git+git://user:password@fake.org/x/y.git#egg=y\")\n    assert (\n        format_requirement(ireq) == \"-e git+git://user:password@fake.org/x/y.git#egg=y\"\n    )\n\n\ndef test_format_requirement_editable_local_path(from_editable):\n    ireq = from_editable(\"file:///home/user/package\")\n    assert format_requirement(ireq) == \"-e file:///home/user/package\"\n\n\ndef test_format_requirement_ireq_with_hashes(from_line):\n    ireq = from_line(\"pytz==2017.2\")\n    ireq_hashes = [\n        \"sha256:d1d6729c85acea5423671382868627129432fba9a89ecbb248d8d1c7a9f01c67\",\n        \"sha256:f5c056e8f62d45ba8215e5cb8f50dfccb198b4b9fbea8500674f3443e4689589\",\n    ]\n\n    expected = (\n        \"pytz==2017.2 \\\\\\n\"\n        \"    --hash=sha256:d1d6729c85acea542367138286\"\n        \"8627129432fba9a89ecbb248d8d1c7a9f01c67 \\\\\\n\"\n        \"    --hash=sha256:f5c056e8f62d45ba8215e5cb8f5\"\n        \"0dfccb198b4b9fbea8500674f3443e4689589\"\n    )\n    assert format_requirement(ireq, hashes=ireq_hashes) == expected\n\n\ndef test_format_requirement_ireq_with_hashes_and_markers(from_line):\n    ireq = from_line(\"pytz==2017.2\")\n    marker = 'python_version<\"3.0\"'\n    ireq_hashes = [\n        \"sha256:d1d6729c85acea5423671382868627129432fba9a89ecbb248d8d1c7a9f01c67\",\n        \"sha256:f5c056e8f62d45ba8215e5cb8f50dfccb198b4b9fbea8500674f3443e4689589\",\n    ]\n\n    expected = (\n        'pytz==2017.2 ; python_version<\"3.0\" \\\\\\n'\n        \"    --hash=sha256:d1d6729c85acea542367138286\"\n        \"8627129432fba9a89ecbb248d8d1c7a9f01c67 \\\\\\n\"\n        \"    --hash=sha256:f5c056e8f62d45ba8215e5cb8f5\"\n        \"0dfccb198b4b9fbea8500674f3443e4689589\"\n    )\n    assert format_requirement(ireq, marker, hashes=ireq_hashes) == expected\n\n\ndef test_format_specifier(from_line, from_editable):\n    ireq = from_line(\"foo\")\n    assert format_specifier(ireq) == \"<any>\"\n\n    ireq = from_line(\"foo==1.2\")\n    assert format_specifier(ireq) == \"==1.2\"\n\n    ireq = from_line(\"foo>1.2,~=1.1,<1.5\")\n    assert format_specifier(ireq) == \"~=1.1,>1.2,<1.5\"\n    ireq = from_line(\"foo~=1.1,<1.5,>1.2\")\n    assert format_specifier(ireq) == \"~=1.1,>1.2,<1.5\"\n\n    ireq = from_editable(\"git+https://github.com/django/django.git#egg=django\")\n    assert format_specifier(ireq) == \"<any>\"\n    ireq = from_editable(\"file:///home/user/package\")\n    assert format_specifier(ireq) == \"<any>\"\n\n\ndef test_as_tuple(from_line):\n    ireq = from_line(\"foo==1.1\")\n    name, version, extras = as_tuple(ireq)\n    assert name == \"foo\"\n    assert version == \"1.1\"\n    assert extras == ()\n\n    ireq = from_line(\"foo[extra1,extra2]==1.1\")\n    name, version, extras = as_tuple(ireq)\n    assert name == \"foo\"\n    assert version == \"1.1\"\n    assert extras == (\"extra1\", \"extra2\")\n\n    # Non-pinned versions aren't accepted\n    should_be_rejected = [\"foo==1.*\", \"foo~=1.1,<1.5,>1.2\", \"foo\"]\n    for spec in should_be_rejected:\n        ireq = from_line(spec)\n        with pytest.raises(TypeError):\n            as_tuple(ireq)\n\n\ndef test_flat_map():\n    assert [1, 2, 4, 1, 3, 9] == list(flat_map(lambda x: [1, x, x * x], [2, 3]))\n\n\ndef test_dedup():\n    assert list(dedup([3, 1, 2, 4, 3, 5])) == [3, 1, 2, 4, 5]\n\n\ndef test_get_hashes_from_ireq(from_line):\n    ireq = from_line(\n        \"pytz==2017.2\",\n        hash_options={\n            \"sha256\": [\n                \"d1d6729c85acea5423671382868627129432fba9a89ecbb248d8d1c7a9f01c67\",\n                \"f5c056e8f62d45ba8215e5cb8f50dfccb198b4b9fbea8500674f3443e4689589\",\n            ]\n        },\n    )\n    expected = {\n        \"sha256:d1d6729c85acea5423671382868627129432fba9a89ecbb248d8d1c7a9f01c67\",\n        \"sha256:f5c056e8f62d45ba8215e5cb8f50dfccb198b4b9fbea8500674f3443e4689589\",\n    }\n    assert get_hashes_from_ireq(ireq) == expected\n\n\n@pytest.mark.parametrize(\n    (\"line\", \"expected\"),\n    (\n        (\"django==1.8\", True),\n        (\"django===1.8\", True),\n        (\"django>1.8\", False),\n        (\"django~=1.8\", False),\n        (\"django==1.*\", False),\n        (\"file:///example.zip\", False),\n        (\"https://example.com/example.zip\", False),\n    ),\n)\ndef test_is_pinned_requirement(from_line, line, expected):\n    ireq = from_line(line)\n    assert is_pinned_requirement(ireq) is expected\n\n\ndef test_is_pinned_requirement_editable(from_editable):\n    ireq = from_editable(\"git+git://fake.org/x/y.git#egg=y\")\n    assert not is_pinned_requirement(ireq)\n\n\ndef test_key_from_ireq_normalization(from_line):\n    keys = set()\n    for line in (\"zope.event\", \"zope-event\", \"zope_event\", \"ZOPE.event\"):\n        keys.add(key_from_ireq(from_line(line)))\n    assert len(keys) == 1\n\n\n@pytest.mark.parametrize(\n    (\"line\", \"expected\"),\n    (\n        (\"build\", \"build\"),\n        (\"cachecontrol[filecache]\", \"cachecontrol\"),\n        (\"some-package[a-b,c_d]\", \"some-package\"),\n        (\"other_package[a.b]\", \"other-package\"),\n    ),\n)\ndef test_key_from_req_on_install_requirement(\n    from_line: _t.Callable[[str], InstallRequirement],\n    line: str,\n    expected: str,\n) -> None:\n    ireq = from_line(line)\n    result = key_from_req(ireq)\n\n    assert result == expected\n\n\n@pytest.mark.parametrize(\n    (\"line\", \"expected\"),\n    (\n        (\"build\", \"build\"),\n        (\"cachecontrol[filecache]\", \"cachecontrol[filecache]\"),\n        (\"some-package[a-b,c_d]\", \"some-package[a-b,c-d]\"),\n        (\"other_package[a.b]\", \"other-package[a-b]\"),\n    ),\n)\ndef test_key_from_req_on_specifier_requirement(\n    from_line: _t.Callable[[str], InstallRequirement],\n    line: str,\n    expected: str,\n) -> None:\n    req = SpecifierRequirement(from_line(line))\n    result = key_from_req(req)\n\n    assert result == expected\n\n\n@pytest.mark.parametrize(\n    (\"line\", \"expected\"),\n    (\n        (\"django==1.8\", False),\n        (\"django\", False),\n        (\"file:///example.zip\", True),\n        (\"https://example.com/example.zip\", True),\n        (\"https://example.com/example.zip#egg=example\", True),\n        (\"git+https://github.com/jazzband/pip-tools@main\", True),\n    ),\n)\ndef test_is_url_requirement(caplog, from_line, line, expected):\n    ireq = from_line(line)\n    assert is_url_requirement(ireq) is expected\n\n\n@pytest.mark.parametrize(\"line\", (\"../example.zip\", \"/example.zip\"))\ndef test_is_url_requirement_filename(caplog, from_line, line):\n    # Ignore warning:\n    #\n    #     Requirement '../example.zip' looks like a filename, but the file does\n    #     not exist\n    caplog.set_level(logging.ERROR, logger=\"pip\")\n    ireq = from_line(line)\n    assert is_url_requirement(ireq) is True\n\n\n@pytest.mark.parametrize(\n    (\"cli_args\", \"expected_command\"),\n    (\n        # Check empty args\n        ([], \"pip-compile\"),\n        # Check all options which will be excluded from command\n        ([\"-v\"], \"pip-compile\"),\n        ([\"--verbose\"], \"pip-compile\"),\n        ([\"-n\"], \"pip-compile\"),\n        ([\"--dry-run\"], \"pip-compile\"),\n        ([\"-q\"], \"pip-compile\"),\n        ([\"--quiet\"], \"pip-compile\"),\n        ([\"-r\"], \"pip-compile\"),\n        ([\"--rebuild\"], \"pip-compile\"),\n        ([\"-U\"], \"pip-compile\"),\n        ([\"--upgrade\"], \"pip-compile\"),\n        ([\"-P\", \"django\"], \"pip-compile\"),\n        ([\"--upgrade-package\", \"django\"], \"pip-compile\"),\n        ([\"--reuse-hashes\"], \"pip-compile\"),\n        ([\"--no-reuse-hashes\"], \"pip-compile\"),\n        # Check options\n        ([\"--max-rounds\", \"42\"], \"pip-compile --max-rounds=42\"),\n        ([\"--index-url\", \"https://foo\"], \"pip-compile --index-url=https://foo\"),\n        # Check that short options will be expanded to long options\n        ([\"-p\"], \"pip-compile --pre\"),\n        ([\"-f\", \"links\"], \"pip-compile --find-links=links\"),\n        ([\"-i\", \"https://foo\"], \"pip-compile --index-url=https://foo\"),\n        # Check positive flags\n        ([\"--generate-hashes\"], \"pip-compile --generate-hashes\"),\n        ([\"--pre\"], \"pip-compile --pre\"),\n        ([\"--allow-unsafe\"], \"pip-compile --allow-unsafe\"),\n        # Check negative flags\n        ([\"--no-emit-index-url\"], \"pip-compile --no-emit-index-url\"),\n        ([\"--no-emit-trusted-host\"], \"pip-compile --no-emit-trusted-host\"),\n        ([\"--no-annotate\"], \"pip-compile --no-annotate\"),\n        ([\"--no-allow-unsafe\"], \"pip-compile\"),\n        ([\"--no-emit-options\"], \"pip-compile --no-emit-options\"),\n        # Check that default values will be removed from the command\n        ([\"--emit-trusted-host\"], \"pip-compile\"),\n        ([\"--emit-options\"], \"pip-compile\"),\n        ([\"--annotate\"], \"pip-compile\"),\n        ([\"--emit-index-url\"], \"pip-compile\"),\n        ([\"--max-rounds=10\"], \"pip-compile\"),\n        ([\"--build-isolation\"], \"pip-compile\"),\n        # Check options with multiple values\n        (\n            [\"--find-links\", \"links1\", \"--find-links\", \"links2\"],\n            \"pip-compile --find-links=links1 --find-links=links2\",\n        ),\n        # Check that option values will be quoted\n        ([\"-f\", \"foo;bar\"], \"pip-compile --find-links='foo;bar'\"),\n        ([\"-f\", \"συνδέσεις\"], \"pip-compile --find-links='συνδέσεις'\"),\n        ([\"-o\", \"my file.txt\"], \"pip-compile --output-file='my file.txt'\"),\n        ([\"-o\", \"απαιτήσεις.txt\"], \"pip-compile --output-file='απαιτήσεις.txt'\"),\n        # Check '--pip-args' (forwarded) arguments\n        (\n            [\"--pip-args\", \"--disable-pip-version-check\"],\n            \"pip-compile --pip-args='--disable-pip-version-check'\",\n        ),\n        (\n            [\"--pip-args\", \"--disable-pip-version-check --isolated\"],\n            \"pip-compile --pip-args='--disable-pip-version-check --isolated'\",\n        ),\n        pytest.param(\n            [\"--extra-index-url\", \"https://username:password@example.com/\"],\n            \"pip-compile --extra-index-url='https://username:****@example.com/'\",\n            id=\"redact password in index\",\n        ),\n        pytest.param(\n            [\"--find-links\", \"https://username:password@example.com/\"],\n            \"pip-compile --find-links='https://username:****@example.com/'\",\n            id=\"redact password in link\",\n        ),\n    ),\n)\ndef test_get_compile_command(tmpdir_cwd, cli_args, expected_command):\n    \"\"\"\n    Test general scenarios for the get_compile_command function.\n    \"\"\"\n    with compile_cli.make_context(\"pip-compile\", cli_args) as ctx:\n        assert get_compile_command(ctx) == expected_command\n\n\n@pytest.mark.parametrize(\n    (\"config_file\", \"expected_command\"),\n    (\n        pytest.param(\n            \"pyproject.toml\", \"pip-compile\", id=\"exclude default pyproject.toml\"\n        ),\n        pytest.param(\n            \".pip-tools.toml\", \"pip-compile\", id=\"exclude default .pip-tools.toml\"\n        ),\n        pytest.param(\n            \"my-config.toml\",\n            \"pip-compile --config=my-config.toml\",\n            id=\"include non-default my-config.toml\",\n        ),\n    ),\n)\ndef test_get_compile_command_with_config(tmpdir_cwd, config_file, expected_command):\n    \"\"\"Test that get_compile_command excludes or includes config file.\"\"\"\n    with open(config_file, \"w\"):\n        pass\n    with compile_cli.make_context(\"pip-compile\", [\"--config\", config_file]) as ctx:\n        assert get_compile_command(ctx) == expected_command\n\n\n@pytest.mark.parametrize(\"config_file\", (\"pyproject.toml\", \".pip-tools.toml\"))\n@pytest.mark.parametrize(\n    \"config_file_content\",\n    (\n        pytest.param(\"\", id=\"empty config file\"),\n        pytest.param(\"[tool.pip-tools]\", id=\"empty config section\"),\n        pytest.param(\"[tool.pip-tools]\\ndry-run = true\", id=\"non-empty config section\"),\n    ),\n)\ndef test_get_compile_command_does_not_include_default_config_if_reqs_file_in_subdir(\n    tmpdir_cwd, config_file, config_file_content\n):\n    \"\"\"\n    Test that ``get_compile_command`` does not include default config file\n    if requirements file is in a subdirectory.\n    Regression test for issue GH-1903.\n    \"\"\"\n    default_config_file = Path(config_file)\n    default_config_file.write_text(config_file_content)\n\n    (tmpdir_cwd / \"subdir\").mkdir()\n    req_file = Path(\"subdir/requirements.in\")\n    req_file.touch()\n    req_file.write_bytes(b\"\")\n\n    # Make sure that the default config file is not included\n    with compile_cli.make_context(\"pip-compile\", [req_file.as_posix()]) as ctx:\n        assert get_compile_command(ctx) == f\"pip-compile {req_file.as_posix()}\"\n\n\ndef test_get_compile_command_escaped_filenames(tmpdir_cwd):\n    \"\"\"\n    Test that get_compile_command output (re-)escapes ' -- '-escaped filenames.\n    \"\"\"\n    with open(\"--requirements.in\", \"w\"):\n        pass\n    with compile_cli.make_context(\"pip-compile\", [\"--\", \"--requirements.in\"]) as ctx:\n        assert get_compile_command(ctx) == \"pip-compile -- --requirements.in\"\n\n\n@pytest.mark.parametrize(\n    \"filename\", (\"requirements.in\", \"my requirements.in\", \"απαιτήσεις.txt\")\n)\ndef test_get_compile_command_with_files(tmpdir_cwd, filename):\n    \"\"\"\n    Test that get_compile_command returns a command with correct\n    and sanitized file names.\n    \"\"\"\n    os.mkdir(\"sub\")\n\n    path = os.path.join(\"sub\", filename)\n    with open(path, \"w\"):\n        pass\n\n    args = [path, \"--output-file\", \"requirements.txt\"]\n    with compile_cli.make_context(\"pip-compile\", args) as ctx:\n        assert (\n            get_compile_command(ctx)\n            == f\"pip-compile --output-file=requirements.txt {shlex.quote(path)}\"\n        )\n\n\ndef test_get_compile_command_sort_args(tmpdir_cwd):\n    \"\"\"\n    Test that get_compile_command correctly sorts arguments.\n\n    The order is \"pip-compile {sorted options} {sorted src files}\".\n    \"\"\"\n    with open(\"setup.py\", \"w\"), open(\"requirements.in\", \"w\"):\n        pass\n\n    args = [\n        \"--no-emit-index-url\",\n        \"--no-emit-trusted-host\",\n        \"--no-annotate\",\n        \"setup.py\",\n        \"--find-links\",\n        \"foo\",\n        \"--find-links\",\n        \"bar\",\n        \"requirements.in\",\n    ]\n    with compile_cli.make_context(\"pip-compile\", args) as ctx:\n        assert get_compile_command(ctx) == (\n            \"pip-compile --find-links=bar --find-links=foo \"\n            \"--no-annotate --no-emit-index-url --no-emit-trusted-host \"\n            \"requirements.in setup.py\"\n        )\n\n\n@pytest.mark.parametrize(\n    \"tuples\",\n    (\n        ((\"f\", \"foo\"), (\"b\", \"bar\"), (\"b\", \"baz\"), (\"q\", \"qux\"), (\"q\", \"quux\")),\n        iter(((\"f\", \"foo\"), (\"b\", \"bar\"), (\"b\", \"baz\"), (\"q\", \"qux\"), (\"q\", \"quux\"))),\n    ),\n)\ndef test_lookup_table_from_tuples(tuples):\n    expected = {\"b\": {\"bar\", \"baz\"}, \"f\": {\"foo\"}, \"q\": {\"quux\", \"qux\"}}\n    assert lookup_table_from_tuples(tuples) == expected\n\n\n@pytest.mark.parametrize(\n    (\"values\", \"key\"),\n    (\n        ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\"), operator.itemgetter(0)),\n        (iter((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\")), operator.itemgetter(0)),\n    ),\n)\ndef test_lookup_table(values, key):\n    expected = {\"b\": {\"bar\", \"baz\"}, \"f\": {\"foo\"}, \"q\": {\"quux\", \"qux\"}}\n    assert lookup_table(values, key) == expected\n\n\ndef test_lookup_table_from_tuples_with_empty_values():\n    assert lookup_table_from_tuples(()) == {}\n\n\ndef test_lookup_table_with_empty_values():\n    assert lookup_table((), operator.itemgetter(0)) == {}\n\n\n@pytest.mark.parametrize(\n    (\"given\", \"expected\"),\n    (\n        (\"\", None),\n        (\"extra == 'dev'\", None),\n        (\"extra == 'dev' or extra == 'test'\", None),\n        (\"os_name == 'nt' and extra == 'dev'\", \"os_name == 'nt'\"),\n        (\"extra == 'dev' and os_name == 'nt'\", \"os_name == 'nt'\"),\n        (\"os_name == 'nt' or extra == 'dev'\", \"os_name == 'nt'\"),\n        (\"extra == 'dev' or os_name == 'nt'\", \"os_name == 'nt'\"),\n        (\"(extra == 'dev') or os_name == 'nt'\", \"os_name == 'nt'\"),\n        (\"os_name == 'nt' and (extra == 'dev' or extra == 'test')\", \"os_name == 'nt'\"),\n        (\"os_name == 'nt' or (extra == 'dev' or extra == 'test')\", \"os_name == 'nt'\"),\n        (\"(extra == 'dev' or extra == 'test') or os_name == 'nt'\", \"os_name == 'nt'\"),\n        (\"(extra == 'dev' or extra == 'test') and os_name == 'nt'\", \"os_name == 'nt'\"),\n        (\n            \"os_name == 'nt' or (os_name == 'unix' and extra == 'test')\",\n            \"os_name == 'nt' or os_name == 'unix'\",\n        ),\n        (\n            \"(os_name == 'unix' and extra == 'test') or os_name == 'nt'\",\n            \"os_name == 'unix' or os_name == 'nt'\",\n        ),\n        (\n            \"(os_name == 'unix' or extra == 'test') and os_name == 'nt'\",\n            \"os_name == 'unix' and os_name == 'nt'\",\n        ),\n        (\n            \"(os_name == 'unix' or os_name == 'nt') and extra == 'dev'\",\n            \"os_name == 'unix' or os_name == 'nt'\",\n        ),\n        (\n            \"(os_name == 'unix' and extra == 'test' or python_version < '3.5')\"\n            \" or os_name == 'nt'\",\n            \"(os_name == 'unix' or python_version < '3.5') or os_name == 'nt'\",\n        ),\n        (\n            \"os_name == 'unix' and extra == 'test' or os_name == 'nt'\",\n            \"os_name == 'unix' or os_name == 'nt'\",\n        ),\n        (\n            \"os_name == 'unix' or extra == 'test' and os_name == 'nt'\",\n            \"os_name == 'unix' or os_name == 'nt'\",\n        ),\n    ),\n)\ndef test_drop_extras(from_line, given, expected):\n    ireq = from_line(f\"test;{given}\")\n    drop_extras(ireq)\n    if expected is None:\n        assert ireq.markers is None\n    else:\n        assert str(ireq.markers).replace(\"'\", '\"') == expected.replace(\"'\", '\"')\n\n\ndef test_get_sys_path_for_python_executable():\n    result = get_sys_path_for_python_executable(sys.executable)\n    assert result, \"get_sys_path_for_python_executable should not return empty result\"\n    # not testing for equality, because pytest adds extra paths into current sys.path\n    for path in result:\n        assert path in sys.path\n\n\n@pytest.mark.parametrize(\n    (\"pyproject_param\", \"new_default\"),\n    (\n        (\"dry-run\", True),\n        (\"find-links\", [\"changed\"]),\n        (\"extra-index-url\", [\"changed\"]),\n        (\"trusted-host\", [\"changed\"]),\n        (\"no-index\", True),\n        (\"verbose\", True),\n        (\"quiet\", True),\n        (\"cert\", \"changed\"),\n        (\"client-cert\", \"changed\"),\n        (\"pip-args\", \"changed\"),\n        (\"pre\", True),\n        (\"rebuild\", True),\n        (\"extra\", [\"changed\"]),\n        (\"all-extras\", True),\n        (\"index-url\", \"changed\"),\n        (\"header\", False),\n        (\"emit-trusted-host\", False),\n        (\"annotate\", False),\n        (\"annotation-style\", \"line\"),\n        (\"upgrade\", True),\n        (\"upgrade-package\", [\"changed\"]),\n        (\"output-file\", \"changed\"),\n        (\"newline\", \"native\"),\n        (\"allow-unsafe\", True),\n        (\"strip-extras\", True),\n        (\"generate-hashes\", True),\n        (\"reuse-hashes\", False),\n        (\"max-rounds\", 100),\n        (\"build-isolation\", False),\n        (\"emit-find-links\", False),\n        (\"cache-dir\", \"changed\"),\n        (\"resolver\", \"backtracking\"),\n        (\"emit-index-url\", False),\n        (\"emit-options\", False),\n        (\"unsafe-package\", [\"changed\"]),\n    ),\n)\ndef test_callback_config_file_defaults(pyproject_param, new_default, make_config_file):\n    config_file = make_config_file(pyproject_param, new_default)\n    # Create a \"compile\" run example pointing to the config file\n    ctx = Context(compile_cli)\n    ctx.params[\"src_files\"] = (str(config_file),)\n    cli_opts = get_cli_options(ctx)\n    found_config_file = override_defaults_from_config_file(ctx, \"config\", None)\n    assert found_config_file == config_file\n    # Make sure the default has been updated\n    lookup_param = cli_opts[\"--\" + pyproject_param].name\n    assert ctx.default_map[lookup_param] == new_default\n\n\n@pytest.mark.parametrize(\n    (\"param\", \"value\"),\n    (\n        (\"extra\", \"not-a-list\"),\n        (\"upgrade_package\", \"not-a-list\"),\n        (\"unsafe_package\", \"not-a-list\"),\n        (\"find_links\", \"not-a-list\"),\n        (\"extra_index_url\", \"not-a-list\"),\n        (\"trusted_host\", \"not-a-list\"),\n        (\"annotate\", \"not-a-bool\"),\n        (\"max_rounds\", \"not-an-int\"),\n        (\"constraint\", \"not-an-list\"),\n    ),\n)\ndef test_callback_config_file_defaults_multi_validate_value(\n    param, value, make_config_file\n):\n    config_file = make_config_file(param, value)\n    ctx = Context(compile_cli)\n    ctx.params[\"src_files\"] = (str(config_file),)\n    with pytest.raises(BadOptionUsage, match=\"Invalid value for config key\"):\n        override_defaults_from_config_file(ctx, \"config\", None)\n\n\ndef test_callback_config_file_defaults_bad_toml(make_config_file):\n    config_file = make_config_file(\"verbose\", True)\n    # Simple means of making invalid TOML: have duplicate keys\n    with Path(config_file).open(\"r+\") as fs:\n        config_text_lines = fs.readlines()\n        fs.write(config_text_lines[-1])\n    ctx = Context(compile_cli)\n    ctx.params[\"src_files\"] = (str(config_file),)\n    with pytest.raises(FileError, match=\"Could not parse \"):\n        override_defaults_from_config_file(ctx, \"config\", None)\n\n\ndef test_callback_config_file_defaults_precedence(make_config_file):\n    piptools_config_file = make_config_file(\"newline\", \"LF\")\n    project_config_file = make_config_file(\"newline\", \"CRLF\", \"pyproject.toml\")\n    ctx = Context(compile_cli)\n    ctx.params[\"src_files\"] = (str(project_config_file),)\n    found_config_file = override_defaults_from_config_file(ctx, \"config\", None)\n    # The pip-tools specific config file should take precedence over pyproject.toml\n    assert found_config_file == piptools_config_file\n    assert ctx.default_map[\"newline\"] == \"LF\"\n\n\ndef test_callback_config_file_defaults_unreadable_toml(make_config_file):\n    ctx = Context(compile_cli)\n    with pytest.raises(FileError, match=\"Could not read \"):\n        override_defaults_from_config_file(\n            ctx,\n            \"config\",\n            \"/dev/null/path/does/not/exist/my-config.toml\",\n        )\n\n\ndef test_select_config_file_no_files(tmpdir_cwd):\n    assert select_config_file(()) is None\n\n\n@pytest.mark.parametrize(\"filename\", (\"pyproject.toml\", \".pip-tools.toml\"))\ndef test_select_config_file_returns_config_in_cwd(make_config_file, filename):\n    config_file = make_config_file(\"dry-run\", True, filename)\n    assert select_config_file(()) == config_file\n\n\ndef test_select_config_file_returns_empty_config_file_in_cwd(tmpdir_cwd):\n    config_file = Path(\".pip-tools.toml\")\n    config_file.touch()\n\n    assert select_config_file(()) == config_file\n\n\ndef test_select_config_file_cannot_find_config_in_cwd(tmpdir_cwd, make_config_file):\n    make_config_file(\"dry-run\", True, \"subdir/pyproject.toml\")\n    assert select_config_file(()) is None\n\n\ndef test_select_config_file_with_config_file_in_subdir(tmpdir_cwd, make_config_file):\n    config_file = make_config_file(\"dry-run\", True, \"subdir/.pip-tools.toml\")\n\n    requirement_file = Path(\"subdir/requirements.in\")\n    requirement_file.touch()\n\n    assert select_config_file((requirement_file.as_posix(),)) == config_file\n\n\ndef test_select_config_file_prefers_pip_tools_toml_over_pyproject_toml(tmpdir_cwd):\n    pip_tools_file = Path(\".pip-tools.toml\")\n    pip_tools_file.touch()\n\n    pyproject_file = Path(\"pyproject.toml\")\n    pyproject_file.write_text(dedent(\"\"\"\\\n            [build-system]\n            requires = [\"setuptools>=63\", \"setuptools_scm[toml]>=7\"]\n            build-backend = \"setuptools.build_meta\"\n            \"\"\"))\n\n    assert select_config_file(()) == pip_tools_file\n"
  },
  {
    "path": "tests/test_writer.py",
    "content": "from __future__ import annotations\n\nimport sys\n\nimport pytest\nfrom pip._internal.models.format_control import FormatControl\n\nfrom piptools.scripts.compile import cli\nfrom piptools.utils import comment\nfrom piptools.writer import (\n    MESSAGE_UNHASHED_PACKAGE,\n    MESSAGE_UNINSTALLABLE,\n    MESSAGE_UNSAFE_PACKAGES,\n    MESSAGE_UNSAFE_PACKAGES_UNPINNED,\n    OutputWriter,\n)\n\n\n@pytest.fixture\ndef writer(tmpdir_cwd):\n    with open(\"src_file\", \"w\"), open(\"src_file2\", \"w\"):\n        pass\n\n    cli_args = [\n        \"--dry-run\",\n        \"--output-file\",\n        \"requirements.txt\",\n        \"src_file\",\n        \"src_file2\",\n    ]\n\n    with cli.make_context(\"pip-compile\", cli_args) as ctx:\n        writer = OutputWriter(\n            dst_file=ctx.params[\"output_file\"],\n            click_ctx=ctx,\n            dry_run=True,\n            emit_header=True,\n            emit_index_url=True,\n            emit_trusted_host=True,\n            annotate=True,\n            annotation_style=\"split\",\n            generate_hashes=False,\n            default_index_url=None,\n            index_urls=[],\n            trusted_hosts=[],\n            format_control=FormatControl(set(), set()),\n            linesep=\"\\n\",\n            allow_unsafe=False,\n            find_links=[],\n            emit_find_links=True,\n            strip_extras=False,\n            emit_options=True,\n        )\n        yield writer\n\n\ndef test_format_requirement_annotation_editable(from_editable, writer):\n    # Annotations are printed as comments at a fixed column\n    ireq = from_editable(\"git+git://fake.org/x/y.git#egg=y\")\n    ireq.comes_from = \"xyz\"\n\n    assert writer._format_requirement(\n        ireq\n    ) == \"-e git+git://fake.org/x/y.git#egg=y\\n    \" + comment(\"# via xyz\")\n\n\ndef test_format_requirement_annotation(from_line, writer):\n    ireq = from_line(\"test==1.2\")\n    ireq.comes_from = \"xyz\"\n\n    assert writer._format_requirement(ireq) == \"test==1.2\\n    \" + comment(\"# via xyz\")\n\n\ndef test_format_requirement_annotation_lower_case(from_line, writer):\n    ireq = from_line(\"Test==1.2\")\n    ireq.comes_from = \"xyz\"\n\n    assert writer._format_requirement(ireq) == \"test==1.2\\n    \" + comment(\"# via xyz\")\n\n\ndef test_format_requirement_for_primary(from_line, writer):\n    \"Primary packages should get annotated.\"\n    ireq = from_line(\"test==1.2\")\n    ireq.comes_from = \"xyz\"\n\n    assert writer._format_requirement(ireq) == \"test==1.2\\n    \" + comment(\"# via xyz\")\n\n\ndef test_format_requirement_for_primary_lower_case(from_line, writer):\n    \"Primary packages should get annotated.\"\n    ireq = from_line(\"Test==1.2\")\n    ireq.comes_from = \"xyz\"\n\n    assert writer._format_requirement(ireq) == \"test==1.2\\n    \" + comment(\"# via xyz\")\n\n\ndef test_format_requirement_environment_marker(from_line, writer):\n    \"Environment markers should get passed through to output.\"\n    ireq = from_line(\n        'test ; python_version == \"2.7\" and platform_python_implementation == \"CPython\"'\n    )\n\n    result = writer._format_requirement(ireq, marker=ireq.markers)\n    assert (\n        result == 'test ; python_version == \"2.7\" and '\n        'platform_python_implementation == \"CPython\"'\n    )\n\n\n@pytest.mark.parametrize(\"allow_unsafe\", ((True,), (False,)))\ndef test_iter_lines__unsafe_dependencies(writer, from_line, allow_unsafe):\n    writer.allow_unsafe = allow_unsafe\n    writer.emit_header = False\n\n    lines = writer._iter_lines(\n        {from_line(\"test==1.2\")},\n        {from_line(\"setuptools==1.10.0\")},\n        unsafe_packages=set(),\n        markers={},\n    )\n\n    expected_lines = (\n        \"test==1.2\",\n        \"\",\n        MESSAGE_UNSAFE_PACKAGES,\n        \"setuptools==1.10.0\" if allow_unsafe else comment(\"# setuptools\"),\n    )\n    assert tuple(lines) == expected_lines\n\n\ndef test_iter_lines__unsafe_with_hashes(capsys, writer, from_line):\n    writer.allow_unsafe = False\n    writer.emit_header = False\n    ireqs = [from_line(\"test==1.2\")]\n    unsafe_ireqs = [from_line(\"setuptools==1.10.0\")]\n    hashes = {ireqs[0]: {\"FAKEHASH\"}, unsafe_ireqs[0]: set()}\n\n    lines = writer._iter_lines(\n        ireqs, unsafe_ireqs, unsafe_packages=set(), markers={}, hashes=hashes\n    )\n\n    expected_lines = (\n        \"test==1.2 \\\\\\n    --hash=FAKEHASH\",\n        \"\",\n        MESSAGE_UNSAFE_PACKAGES_UNPINNED,\n        comment(\"# setuptools\"),\n    )\n    assert tuple(lines) == expected_lines\n    captured = capsys.readouterr()\n    assert captured.out == \"\"\n    assert captured.err.strip() == MESSAGE_UNINSTALLABLE\n\n\ndef test_iter_lines__hash_missing(capsys, writer, from_line):\n    writer.allow_unsafe = False\n    writer.emit_header = False\n    ireqs = [from_line(\"test==1.2\"), from_line(\"file:///example/#egg=example\")]\n    hashes = {ireqs[0]: {\"FAKEHASH\"}, ireqs[1]: set()}\n\n    lines = writer._iter_lines(\n        ireqs,\n        hashes=hashes,\n        unsafe_requirements=set(),\n        unsafe_packages=set(),\n        markers={},\n    )\n\n    expected_lines = (\n        MESSAGE_UNHASHED_PACKAGE,\n        \"example @ file:///example/\",\n        \"test==1.2 \\\\\\n    --hash=FAKEHASH\",\n    )\n    assert tuple(lines) == expected_lines\n    captured = capsys.readouterr()\n    assert captured.out == \"\"\n    assert captured.err.strip() == MESSAGE_UNINSTALLABLE\n\n\ndef test_iter_lines__no_warn_if_only_unhashable_packages(writer, from_line):\n    \"\"\"\n    There shouldn't be MESSAGE_UNHASHED_PACKAGE warning if there are only unhashable\n    packages. See GH-1101.\n    \"\"\"\n    writer.allow_unsafe = False\n    writer.emit_header = False\n    ireqs = [\n        from_line(\"file:///unhashable-pkg1/#egg=unhashable-pkg1\"),\n        from_line(\"file:///unhashable-pkg2/#egg=unhashable-pkg2\"),\n    ]\n    hashes = {ireq: set() for ireq in ireqs}\n\n    lines = writer._iter_lines(\n        ireqs,\n        hashes=hashes,\n        unsafe_requirements=set(),\n        unsafe_packages=set(),\n        markers={},\n    )\n\n    expected_lines = (\n        \"unhashable-pkg1 @ file:///unhashable-pkg1/\",\n        \"unhashable-pkg2 @ file:///unhashable-pkg2/\",\n    )\n    assert tuple(lines) == expected_lines\n\n\ndef test_write_header(writer):\n    expected = map(\n        comment,\n        [\n            \"#\",\n            \"# This file is autogenerated by pip-compile with Python \"\n            f\"{sys.version_info.major}.{sys.version_info.minor}\",\n            \"# by the following command:\",\n            \"#\",\n            \"#    pip-compile --output-file={} src_file src_file2\".format(\n                writer.click_ctx.params[\"output_file\"].name\n            ),\n            \"#\",\n        ],\n    )\n    assert list(writer.write_header()) == list(expected)\n\n\ndef test_write_header_custom_compile_command(writer, monkeypatch):\n    monkeypatch.setenv(\"CUSTOM_COMPILE_COMMAND\", \"./pipcompilewrapper\")\n    expected = map(\n        comment,\n        [\n            \"#\",\n            \"# This file is autogenerated by pip-compile with Python \"\n            f\"{sys.version_info.major}.{sys.version_info.minor}\",\n            \"# by the following command:\",\n            \"#\",\n            \"#    ./pipcompilewrapper\",\n            \"#\",\n        ],\n    )\n    assert list(writer.write_header()) == list(expected)\n\n\ndef test_write_header_no_emit_header(writer):\n    \"\"\"\n    There should not be headers if emit_header is False\n    \"\"\"\n    writer.emit_header = False\n\n    with pytest.raises(StopIteration):\n        next(writer.write_header())\n\n\n@pytest.mark.parametrize(\n    (\"emit_options\", \"expected_flags\"),\n    (\n        pytest.param(\n            True,\n            (\n                \"--index-url https://index-server\",\n                \"--find-links links\",\n                \"--trusted-host index-server\",\n                \"--no-binary flask\",\n                \"--only-binary django\",\n                \"\",\n            ),\n            id=\"on\",\n        ),\n        pytest.param(False, (), id=\"off\"),\n    ),\n)\ndef test_write_flags_emit_options(writer, emit_options, expected_flags):\n    \"\"\"\n    There should be options if emit_options is True\n    \"\"\"\n    writer.emit_options = emit_options\n    writer.index_urls = [\"https://index-server\"]\n    writer.find_links = [\"links\"]\n    writer.trusted_hosts = [\"index-server\"]\n    writer.format_control = FormatControl(no_binary=[\"flask\"], only_binary=[\"django\"])\n\n    assert tuple(writer.write_flags()) == expected_flags\n\n\ndef test_write_format_controls(writer):\n    \"\"\"\n    Tests --no-binary/--only-binary options.\n    \"\"\"\n\n    # FormatControl actually expects sets, but we give it lists here to\n    # ensure that we are sorting them when writing.\n    writer.format_control = FormatControl(\n        no_binary=[\"psycopg2\", \"click\"], only_binary=[\"pytz\", \"django\"]\n    )\n    lines = list(writer.write_format_controls())\n\n    expected_lines = [\n        \"--no-binary click\",\n        \"--no-binary psycopg2\",\n        \"--only-binary django\",\n        \"--only-binary pytz\",\n    ]\n    assert lines == expected_lines\n\n\n@pytest.mark.parametrize(\n    (\"no_binary\", \"only_binary\", \"expected_lines\"),\n    (\n        (\n            [\":all:\"],\n            [\"django\"],\n            [\n                \"--no-binary :all:\",\n                \"--only-binary django\",\n            ],\n        ),\n        (\n            [\"django\"],\n            [\":all:\"],\n            [\n                \"--only-binary :all:\",\n                \"--no-binary django\",\n            ],\n        ),\n    ),\n)\ndef test_write_format_controls_all(writer, no_binary, only_binary, expected_lines):\n    \"\"\"\n    Tests --no-binary/--only-binary options\n    with the value of :all:. We want to preserve\n    the FormatControl behavior so we emit :all:\n    first before packages.\n    \"\"\"\n\n    writer.format_control = FormatControl(no_binary=no_binary, only_binary=only_binary)\n    lines = list(writer.write_format_controls())\n\n    assert lines == expected_lines\n\n\n@pytest.mark.parametrize(\n    (\"index_urls\", \"expected_lines\"),\n    (\n        # No urls - no options\n        ([], []),\n        # Single URL should be index-url\n        ([\"https://index-url.com\"], [\"--index-url https://index-url.com\"]),\n        # First URL should be index-url, the others should be extra-index-url\n        (\n            [\n                \"https://index-url1.com\",\n                \"https://index-url2.com\",\n                \"https://index-url3.com\",\n            ],\n            [\n                \"--index-url https://index-url1.com\",\n                \"--extra-index-url https://index-url2.com\",\n                \"--extra-index-url https://index-url3.com\",\n            ],\n        ),\n        # If a first URL equals to the default URL, the the index url must not be set\n        # and the others should be extra-index-url\n        (\n            [\n                \"https://default-index-url.com\",\n                \"https://index-url1.com\",\n                \"https://index-url2.com\",\n            ],\n            [\n                \"--extra-index-url https://index-url1.com\",\n                \"--extra-index-url https://index-url2.com\",\n            ],\n        ),\n        # Not ignore URLs equal to the default index-url\n        # (note: the previous case is exception)\n        (\n            [\n                \"https://index-url1.com\",\n                \"https://default-index-url.com\",\n                \"https://index-url2.com\",\n            ],\n            [\n                \"--index-url https://index-url1.com\",\n                \"--extra-index-url https://default-index-url.com\",\n                \"--extra-index-url https://index-url2.com\",\n            ],\n        ),\n        # Ignore URLs equal to the default index-url\n        ([\"https://default-index-url.com\", \"https://default-index-url.com\"], []),\n        # All URLs must be deduplicated\n        (\n            [\n                \"https://index-url1.com\",\n                \"https://index-url1.com\",\n                \"https://index-url2.com\",\n            ],\n            [\n                \"--index-url https://index-url1.com\",\n                \"--extra-index-url https://index-url2.com\",\n            ],\n        ),\n    ),\n)\ndef test_write_index_options(writer, index_urls, expected_lines):\n    \"\"\"\n    Test write_index_options method.\n    \"\"\"\n    writer.index_urls = index_urls\n    writer.default_index_url = \"https://default-index-url.com\"\n    assert list(writer.write_index_options()) == expected_lines\n\n\ndef test_write_index_options_no_emit_index(writer):\n    \"\"\"\n    There should not be --index-url/--extra-index-url options\n    if emit_index_url is False.\n    \"\"\"\n    writer.emit_index_url = False\n    with pytest.raises(StopIteration):\n        next(writer.write_index_options())\n\n\n@pytest.mark.parametrize(\n    (\"find_links\", \"expected_lines\"),\n    (\n        ([], []),\n        ([\"./foo\"], [\"--find-links ./foo\"]),\n        ([\"./foo\", \"./bar\"], [\"--find-links ./foo\", \"--find-links ./bar\"]),\n    ),\n)\ndef test_write_find_links(writer, find_links, expected_lines):\n    \"\"\"\n    Test write_find_links method.\n    \"\"\"\n    writer.find_links = find_links\n    assert list(writer.write_find_links()) == expected_lines\n\n\ndef test_write_order(writer, from_line):\n    \"\"\"\n    Order of packages should match that of `pip freeze`, with the exception\n    that requirement names should be canonicalized.\n    \"\"\"\n    writer.emit_header = False\n\n    packages = {\n        from_line(\"package_a==0.1\"),\n        from_line(\"Package-b==2.3.4\"),\n        from_line(\"Package==5.6\"),\n        from_line(\"package2==7.8.9\"),\n    }\n    expected_lines = [\n        \"package==5.6\",\n        \"package-a==0.1\",\n        \"package-b==2.3.4\",\n        \"package2==7.8.9\",\n    ]\n    result = writer._iter_lines(\n        packages, unsafe_requirements=set(), unsafe_packages=set(), markers={}\n    )\n    assert list(result) == expected_lines\n"
  },
  {
    "path": "tests/unit/_internal/pip_api/test_cli_options.py",
    "content": "from __future__ import annotations\n\nimport pytest\nfrom pip._internal.commands import create_command\n\nfrom piptools._internal import _pip_api\n\n\n@pytest.mark.skipif(\n    _pip_api.PIP_VERSION_MAJOR_MINOR < (26, 0), reason=\"test requires pip>=26.0\"\n)\ndef test_postprocess_cli_options_pre_adds_all_to_release_control_all_releases():\n    \"\"\"\n    Test that ``--pre`` gets transformed into ``--all-releases :all:``\n    \"\"\"\n    # start with pip's own parsing logic (as applied by the PyPIRepository)\n    cmd = create_command(\"install\")\n    options, _ = cmd.parse_args([\"--pre\"])\n\n    # initiall, 'all_releases' is empty\n    assert not options.release_control.all_releases\n\n    _pip_api.postprocess_cli_options(options)\n    assert \":all:\" in options.release_control.all_releases\n"
  },
  {
    "path": "tests/unit/_internal/pip_api/test_install_requirements.py",
    "content": "from __future__ import annotations\n\nimport pytest\n\nfrom piptools._internal import _pip_api\n\n\n@pytest.mark.skipif(\n    _pip_api.PIP_VERSION_MAJOR_MINOR < (25, 3), reason=\"test requires pip>=25.3\"\n)\n@pytest.mark.parametrize(\"use_pep517\", (True, False))\ndef test_copy_install_requirement_removes_pip_25_3_unsupported_opts(use_pep517):\n    req = _pip_api.create_install_requirement_from_line(\"foolib==0.1\")\n\n    updated_req = _pip_api.copy_install_requirement(req, use_pep517=use_pep517)\n    assert not hasattr(updated_req, \"use_pep517\")\n\n\n@pytest.mark.skipif(\n    _pip_api.PIP_VERSION_MAJOR_MINOR >= (25, 3), reason=\"test requires pip<25.3\"\n)\n@pytest.mark.parametrize(\"use_pep517\", (True, False))\ndef test_copy_install_requirement_preserves_pip_25_3_unsupported_opts(use_pep517):\n    req = _pip_api.create_install_requirement_from_line(\"foolib==0.1\")\n\n    updated_req = _pip_api.copy_install_requirement(req, use_pep517=use_pep517)\n    assert hasattr(updated_req, \"use_pep517\")\n    assert updated_req.use_pep517 == use_pep517\n"
  },
  {
    "path": "tests/unit/_internal/pip_api/test_package_finder.py",
    "content": "from __future__ import annotations\n\nimport pytest\n\nfrom piptools._internal import _pip_api\nfrom piptools.repositories import PyPIRepository\n\n\n@pytest.fixture\ndef finder_with_pre(tmp_path):\n    # PyPIRepository init is the primary way that a PackageFinder gets build\n    # in piptools, so we use it for this fixture\n    repo = PyPIRepository([\"--pre\"], cache_dir=tmp_path / \"cache_dir\")\n    return repo._finder\n\n\ndef test_finder_with_pre_allows_all_prereleases(finder_with_pre):\n    assert _pip_api.finder_allows_all_prereleases(finder_with_pre)\n\n\ndef test_finder_with_pre_allows_specific_package_prereleases(finder_with_pre):\n    req = _pip_api.create_install_requirement_from_line(\"foolib>1\")\n    assert _pip_api.finder_allows_prereleases_of_req(finder_with_pre, req)\n"
  },
  {
    "path": "tests/unit/_internal/pip_api/test_pip_version.py",
    "content": "from __future__ import annotations\n\nimport sys\n\nimport pip\nfrom pip._vendor.packaging.version import Version\n\nfrom piptools._internal import _pip_api\n\n\ndef test_get_pip_version_for_python_executable():\n    result = _pip_api.get_pip_version_for_python_executable(sys.executable)\n    assert Version(pip.__version__) == result\n"
  },
  {
    "path": "tests/unit/_internal/test_subprocess.py",
    "content": "from __future__ import annotations\n\nimport sys\n\nfrom piptools._internal import _subprocess\n\n\ndef test_run_python_snippet_returns_multilne():\n    result = _subprocess.run_python_snippet(\n        sys.executable, r'print(\"MULTILINE\\nOUTPUT\", end=\"\")'\n    )\n    assert result == \"MULTILINE\\nOUTPUT\"\n"
  },
  {
    "path": "tests/utils.py",
    "content": "from __future__ import annotations\n\nimport os\n\n# NOTE: keep in sync with \"passenv\" in tox.ini\nCI_VARIABLES = {\"CI\", \"GITHUB_ACTIONS\"}\n\n\ndef looks_like_ci():\n    return bool(set(os.environ.keys()) & CI_VARIABLES)\n"
  },
  {
    "path": "towncrier.toml",
    "content": "[tool.towncrier]\n  package = \"piptools\"\n  filename = \"CHANGELOG.md\"\n  start_string = \"<!-- towncrier release notes start -->\\n\"\n  directory = \"changelog.d/\"\n  title_format = \"\"\n  template = \"changelog.d/.towncrier_template.md.jinja\"\n  # the issue format is bare here, but then handled in the template\n  # see changelog.d/.towncrier_template.md.jinja for details\n  issue_format = \"{issue}\"\n  underlines = [\"\", \"\", \"\"]\n\n  [[tool.towncrier.section]]\n    path = \"\"\n\n  # Fully redeclare towncrier types to control names and set 'showcontent'\n\n  [[tool.towncrier.type]]\n    # A prelude to the release notes.\n    directory = \"highlights\"\n    name = \"Foreword\"\n    showcontent = true\n\n  [[tool.towncrier.type]]\n    # Improper/undesired behavior that got corrected.\n    directory = \"bugfix\"\n    name = \"Bug fixes\"\n    showcontent = true\n\n  [[tool.towncrier.type]]\n    # New behaviors, CLI flags, etc.\n    directory = \"feature\"\n    name = \"Features\"\n    showcontent = true\n\n  [[tool.towncrier.type]]\n    # Declarations of future removals and breaking changes in behavior.\n    directory = \"deprecation\"\n    name = \"Deprecations (removal in next major release)\"\n    showcontent = true\n\n  [[tool.towncrier.type]]\n    # A change in the behavior of a command, such that users may observe the\n    # change purely via their usage and be negatively impacted.\n    directory = \"breaking\"\n    name = \"Removals and backward incompatible breaking changes\"\n    showcontent = true\n\n  [[tool.towncrier.type]]\n    # Notable updates to the documentation structure or build process.\n    directory = \"doc\"\n    name = \"Improved documentation\"\n    showcontent = true\n\n  [[tool.towncrier.type]]\n    # Notes for downstreams about unobvious side effects and tooling. Changes\n    # in the test invocation considerations and runtime assumptions.\n    directory = \"packaging\"\n    name = \"Packaging updates and notes for downstreams\"\n    showcontent = true\n\n  [[tool.towncrier.type]]\n    # Stuff that affects the contributor experience. e.g. Running tests,\n    # building the docs, setting up the development environment.\n    directory = \"contrib\"\n    name = \"Contributor-facing changes\"\n    showcontent = true\n\n  [[tool.towncrier.type]]\n    # Changes that are hard to assign to any of the above categories.\n    directory = \"misc\"\n    name = \"Miscellaneous internal changes\"\n    showcontent = true\n\n  [[tool.towncrier.type]]\n    # A concluding paragraph after the release notes.\n    directory = \"afterword\"\n    name = \"Afterword\"\n    showcontent = true\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist =\n    # NOTE: keep this in sync with the env list in .github/workflows/ci.yml.\n    py{39,310,311,312,313,314,314t,py3}-pip{supported,lowest,latest,main}-coverage\n    pip{supported,lowest,latest,main}-coverage\n    pip{supported,lowest,latest,main}\n    checkqa\n    readme\nskip_missing_interpreters = True\n\n[python-cli-options]\nbyte-warnings = -b\nbyte-errors = -bb\nmax-isolation = -E -s -I\n# some-isolation = -I\n# FIXME: Python 2 shim. Is this equivalent to the above?\nsome-isolation = -E -s\nwarnings-to-errors = -Werror\n\n\n[testenv]\ndescription = run the tests with pytest\nextras =\n    testing\n    coverage: coverage\ndeps =\n    pipsupported: pip == 26.0\n    pipsupported: setuptools <= 80.0\n\n    piplowest: pip == 22.2.* ; python_version < \"3.12\"\n    piplowest: pip == 23.2.* ; python_version >= \"3.12\" and python_version < \"3.14\"\n    piplowest: pip == 25.2.* ; python_version >= \"3.14\"\n\n    piplatest: pip\n    pipmain: https://github.com/pypa/pip/archive/main.zip\nsetenv =\n    coverage: PYTEST_ADDOPTS=--strict-markers --doctest-modules --cov --cov-report=term-missing --cov-report=xml {env:PYTEST_ADDOPTS:}\ncommands_pre =\n    piplatest: python -m pip install -U pip\n    pip --version\ncommands = pytest {posargs}\npassenv =\n    CI\n    FORCE_COLOR\n    GITHUB_ACTIONS\n    MYPY_FORCE_COLOR\n    PRE_COMMIT_COLOR\n    PY_COLORS\npip_pre=True\n\n[testenv:checkqa]\ndescription = format the code base and check its quality\nskip_install = True\ndeps = pre-commit\ncommands_pre =\ncommands = pre-commit run --all-files --show-diff-on-failure\n\n[testenv:readme]\ndescription = check whether the long description will render correctly on PyPI\ndeps =\n    build\n    twine\ncommands_pre =\ncommands =\n    python -m build --outdir {envtmpdir} --sdist {toxinidir}\n    twine check --strict {envtmpdir}{/}*\nskip_install = true\n\n[testenv:pip-compile-docs]\ndescription = compile requirements for the documentation\ncommands_pre =\n# compile requirements.in + pyproject.toml to get pip-tools install requirements in\n# addition to doc tooling requirements *but without* putting `pip-tools` itself\n# into the output\ncommands =\n  python -m piptools compile \\\n    --strip-extras \\\n    --allow-unsafe \\\n    --quiet \\\n    docs/requirements.in \\\n    './pyproject.toml' \\\n    -o docs/requirements.txt \\\n    {posargs}\n\n[testenv:build-docs]\ndescription = build the documentation\ndeps =\n  -r{toxinidir}/docs/requirements.txt\n  # FIXME: re-enable the \"-r\" + \"-c\" paradigm once the pip bug is fixed.\n  # Ref: https://github.com/pypa/pip/issues/9243\n  # -r{toxinidir}/docs/requirements.in\n  # -c{toxinidir}/docs/requirements.txt\ncommands_pre =\n  # Retrieve possibly missing commits:\n  -git fetch --unshallow\n  -git fetch --tags\ncommands =\n  # Build the html docs with Sphinx:\n  {envpython} -m sphinx \\\n    -j auto \\\n    -b html \\\n    {tty:--color} \\\n    -a \\\n    -n -W --keep-going \\\n    -d \"{temp_dir}/.doctrees\" \\\n    . \\\n    {posargs:{envdir}/docs_out}\n# Print out the output docs dir and a way to serve html after the build\ncommands_post =\n  {envpython} -c \\\n    'import pathlib;\\\n    docs_dir = pathlib.Path(r\"{envdir}\") / \"docs_out\";\\\n    index_file = docs_dir / \"index.html\";\\\n    print(\"\\n\" + \"=\" * 120 +\\\n    f\"\\n\\nDocumentation available under:\\n\\n\\\n    \\tfile://\\{index_file\\}\\n\\nTo serve docs, use\\n\\n\\\n    \\t$ python3 -m http.server --directory \\\n    \\N\\{QUOTATION MARK\\}\\{docs_dir\\}\\N\\{QUOTATION MARK\\} 0\\n\\n\" +\\\n    \"=\" * 120)'\nchangedir = {toxinidir}/docs\nisolated_build = true\npassenv =\n  READTHEDOCS\n  READTHEDOCS_VERSION_TYPE\n  SSH_AUTH_SOCK\nskip_install = false\nallowlist_externals =\n  git\n\n\n[testenv:preview-docs]\ndescription = preview the docs\ndeps =\n  sphinx-autobuild\n  {[testenv:build-docs]deps}\ncommands_pre =\ncommands =\n  # Retrieve possibly missing commits:\n  -git fetch --unshallow\n  -git fetch --tags\n\n  # Build the html docs with sphinx-autobuild:\n  {envpython} -m sphinx_autobuild \\\n    -j auto \\\n    -b html \\\n    -n \\\n    -W \\\n    -d \"{temp_dir}/.doctrees\" \\\n    . \\\n    --watch ../README.md \\\n    --watch ../CHANGELOG.md \\\n    \"{envdir}/docs_out\"\n\nchangedir = {[testenv:build-docs]changedir}\nisolated_build = {[testenv:build-docs]isolated_build}\npassenv = {[testenv:build-docs]passenv}\nskip_install = {[testenv:build-docs]skip_install}\nallowlist_externals = {[testenv:build-docs]allowlist_externals}\n\n\n[testenv:linkcheck-docs]\ndescription = check links in the documentation\ndeps =\n  -r{toxinidir}/docs/requirements.txt\n  # FIXME: re-enable the \"-r\" + \"-c\" paradigm once the pip bug is fixed.\n  # Ref: https://github.com/pypa/pip/issues/9243\n  # -r{toxinidir}/docs/requirements.in\n  # -c{toxinidir}/docs/requirements.txt\ncommands_pre =\ncommands =\n  # Retrieve possibly missing commits:\n  -git fetch --unshallow\n  -git fetch --tags\n\n  # Build the html docs with Sphinx:\n  {envpython} -m sphinx \\\n    -j auto \\\n    -b linkcheck \\\n    {tty:--color} \\\n    -a \\\n    -n -W --keep-going \\\n    -d \"{temp_dir}/.doctrees\" \\\n    . \\\n    \"{envdir}/docs_out\"\nchangedir = {toxinidir}/docs\nisolated_build = true\npassenv =\n  SSH_AUTH_SOCK\nskip_install = false\nallowlist_externals =\n  git\n\n[testenv:changelog-draft]\ndeps =\n  towncrier\n  -c{toxinidir}/docs/requirements.txt\ncommands_pre =\n  towncrier --version\n# wrap the invocation of `towncrier build --version main --draft` to discard stderr\n# unfortunately, stderr gets interleaved with stdout, often mixing towncrier's draft build output\n# with user messaging about the invocation\ncommands =\n  {envpython} -bb -I -Werror \\\n    -c 'import sys, subprocess; subprocess.run([sys.executable, \"-bb\", \"-I\", \"-Werror\", \"-m\", \"towncrier\", \"build\", \"--version\", \"DRAFT_VERSION\", \"--draft\", *sys.argv[1:]], stderr=subprocess.DEVNULL)' {posargs}\n\n\n[testenv:cleanup-dists]\ndescription =\n  Wipe the dist{/} folder\ndeps =\ncommands_pre =\ncommands =\n  {envpython} \\\n    {[python-cli-options]byte-errors} \\\n    {[python-cli-options]max-isolation} \\\n    {[python-cli-options]warnings-to-errors} \\\n    -c \\\n      'import os, shutil, sys; \\\n      dists_dir = \"{toxinidir}{/}dist{/}\"; \\\n      shutil.rmtree(dists_dir, ignore_errors=True); \\\n      sys.exit(os.path.exists(dists_dir))'\ncommands_post =\npackage = skip\n\n\n[testenv:build-dists]\nallowlist_externals =\n  env\ndescription =\n  Build dists with {basepython} and put them into the dist{/} folder\ndepends =\n  cleanup-dists\ndeps =\n  build\ncommands_pre =\ncommands =\n  {envpython} \\\n    {[python-cli-options]byte-errors} \\\n    {[python-cli-options]max-isolation} \\\n    {[python-cli-options]warnings-to-errors} \\\n    -m build \\\n      {posargs:}\ncommands_post =\npackage = skip\n\n\n[testenv:metadata-validation]\ndescription =\n  Verify that dists under the `dist{/}` dir\n  have valid metadata\ndepends =\n  build-dists\ndeps =\n  twine\ncommands =\n  {envpython} \\\n    {[python-cli-options]byte-errors} \\\n    {[python-cli-options]max-isolation} \\\n    {[python-cli-options]warnings-to-errors} \\\n    -m twine \\\n      check \\\n      --strict \\\n      dist{/}*\ncommands_post =\npackage = skip\n"
  }
]