[
  {
    "path": ".github/release.py",
    "content": "\"\"\"Create a new release tag with CalVer format.\"\"\"\n\nimport datetime\nimport operator\nimport os\nfrom pathlib import Path\n\nimport git\nfrom packaging import version\n\n\ndef get_repo() -> git.Repo:\n    \"\"\"Get the git repo for the current project.\"\"\"\n    return git.Repo(Path(__file__).parent.parent)\n\n\ndef is_already_tagged(repo: git.Repo) -> bool:\n    \"\"\"Check if the current commit is already tagged.\"\"\"\n    return repo.git.tag(points_at=\"HEAD\")\n\n\ndef should_skip_release(repo: git.Repo) -> bool:\n    \"\"\"Check if the commit message contains [skip release].\"\"\"\n    commit_message = repo.head.commit.message.split(\"\\n\")[0]\n    return \"[skip release]\" in commit_message\n\n\ndef get_new_version(repo: git.Repo) -> str:\n    \"\"\"Get the new version number.\"\"\"\n    latest_tag = max(repo.tags, key=operator.attrgetter(\"commit.committed_datetime\"))\n    last_version = version.parse(latest_tag.name)\n    now = datetime.datetime.now(tz=datetime.timezone.utc)\n    patch = (\n        last_version.micro + 1\n        if last_version.major == now.year and last_version.minor == now.month\n        else 0\n    )\n    return f\"{now.year}.{now.month}.{patch}\"\n\n\ndef set_author(repo: git.Repo) -> None:\n    \"\"\"Set author information.\"\"\"\n    author_name = repo.head.commit.author.name\n    author_email = repo.head.commit.author.email\n    os.environ[\"GIT_AUTHOR_NAME\"] = author_name\n    os.environ[\"GIT_AUTHOR_EMAIL\"] = author_email\n    os.environ[\"GIT_COMMITTER_NAME\"] = author_name\n    os.environ[\"GIT_COMMITTER_EMAIL\"] = author_email\n\n\ndef create_tag(repo: git.Repo, new_version: str, release_notes: str) -> None:\n    \"\"\"Create a new tag.\"\"\"\n    set_author(repo)\n    repo.create_tag(new_version, message=f\"Release {new_version}\\n\\n{release_notes}\")\n\n\ndef push_tag(repo: git.Repo, new_version: str) -> None:\n    \"\"\"Push the new tag to the remote repository.\"\"\"\n    origin = repo.remote(\"origin\")\n    origin.push(new_version)\n\n\ndef get_commit_messages_since_last_release(repo: git.Repo) -> str:\n    \"\"\"Get the commit messages since the last release.\"\"\"\n    latest_tag = max(repo.tags, key=operator.attrgetter(\"commit.committed_datetime\"))\n    return repo.git.log(f\"{latest_tag}..HEAD\", \"--pretty=format:%s\")\n\n\ndef format_release_notes(commit_messages: str, new_version: str) -> str:\n    \"\"\"Format the release notes.\"\"\"\n    header = f\"🚀 Release {new_version}\\n\\n\"\n    intro = \"📝 This release includes the following changes:\\n\\n\"\n\n    commit_list = commit_messages.split(\"\\n\")\n    formatted_commit_list = [f\"- {commit}\" for commit in commit_list]\n    commit_section = \"\\n\".join(formatted_commit_list)\n\n    footer = (\n        \"\\n\\n🙏 Thank you for using this project! Please report any issues \"\n        \"or feedback on the GitHub repository\"\n        \" on https://github.com/basnijholt/home-assistant-streamdeck-yaml.\"\n    )\n\n    return f\"{header}{intro}{commit_section}{footer}\"\n\n\ndef main() -> None:\n    \"\"\"Main entry point.\"\"\"\n    repo = get_repo()\n    if is_already_tagged(repo):\n        print(\"Current commit is already tagged!\")\n        return\n\n    if should_skip_release(repo):\n        print(\"Commit message is [skip release]!\")\n        return\n\n    new_version = get_new_version(repo)\n    commit_messages = get_commit_messages_since_last_release(repo)\n    release_notes = format_release_notes(commit_messages, new_version)\n    print(release_notes)\n    create_tag(repo, new_version, release_notes)\n    push_tag(repo, new_version)\n    # Write the output version to the GITHUB_OUTPUT environment file\n    with open(os.environ[\"GITHUB_OUTPUT\"], \"a\") as output_file:  # noqa: PTH123\n        output_file.write(f\"version={new_version}\\n\")\n    print(f\"Created new tag: {new_version}\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": ".github/renovate.json",
    "content": "{\n    \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n    \"rebaseWhen\": \"behind-base-branch\",\n    \"dependencyDashboard\": true,\n    \"labels\": [\n        \"dependencies\",\n        \"no-stale\"\n    ],\n    \"commitMessagePrefix\": \"⬆️\",\n    \"commitMessageTopic\": \"{{depName}}\",\n    \"prBodyDefinitions\": {\n        \"Release\": \"yes\"\n    },\n    \"packageRules\": [\n        {\n            \"matchManagers\": [\n                \"github-actions\"\n            ],\n            \"addLabels\": [\n                \"github_actions\"\n            ],\n            \"rangeStrategy\": \"pin\"\n        },\n        {\n            \"matchManagers\": [\n                \"github-actions\"\n            ],\n            \"matchUpdateTypes\": [\n                \"minor\",\n                \"patch\"\n            ],\n            \"automerge\": true\n        }\n    ]\n}\n"
  },
  {
    "path": ".github/use-local-unidep.py",
    "content": "\"\"\"Update `pyproject.toml` in each example project to use local `unidep`.\"\"\"\n\nfrom pathlib import Path\n\nREPO_ROOT = Path(__file__).resolve().parent.parent\nEXAMPLE_DIR = REPO_ROOT / \"example\"\nPROJECT_DIRS = [p for p in EXAMPLE_DIR.iterdir() if p.name.endswith(\"_project\")]\nREPO_ROOT_URI = REPO_ROOT.resolve().as_uri()\n\nprint(\n    f\"REPO_ROOT: {REPO_ROOT}, EXAMPLE_DIR: {EXAMPLE_DIR}, PROJECT_DIRS: {PROJECT_DIRS}\",\n)\n\nfor project_dir in PROJECT_DIRS:\n    # Find the line with `requires = [` in `pyproject.toml` and replace\n    # `unidep`/`unidep[toml]` entries with file:// references to the repo root.\n    pyproject_toml = project_dir / \"pyproject.toml\"\n    lines = pyproject_toml.read_text().splitlines()\n    for i, line in enumerate(lines):\n        if \"requires = [\" not in line:\n            continue\n        if \"unidep[toml]\" in line:\n            lines[i] = line.replace(\n                \"unidep[toml]\",\n                f\"unidep[toml] @ {REPO_ROOT_URI}\",\n            )\n        elif \"unidep\" in line:\n            lines[i] = line.replace(\"unidep\", f\"unidep @ {REPO_ROOT_URI}\")\n        break\n    pyproject_toml.write_text(\"\\n\".join(lines))\n"
  },
  {
    "path": ".github/workflows/documentation-links.yml",
    "content": "name: readthedocs/actions\non:\n  pull_request_target:\n    types:\n      - opened\n\npermissions:\n  pull-requests: write\n\njobs:\n  documentation-links:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: readthedocs/actions/preview@v1\n        with:\n          project-slug: \"unidep\"\n"
  },
  {
    "path": ".github/workflows/install-example-projects.yml",
    "content": "name: install-example-projects\n\non:\n  push:\n    branches: [main]\n  pull_request:\n\njobs:\n  pip-install:\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [\"3.8\", \"3.9\", \"3.10\", \"3.11\", \"3.12\"] # skips 3.7 (unsupported on GH Actions)\n        platform: [ubuntu-latest, macos-latest, windows-latest]\n    runs-on: ${{ matrix.platform }}\n    env:\n      PYTHONIOENCODING: \"utf8\" # https://gist.github.com/NodeJSmith/e7e37f2d3f162456869f015f842bcf15\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n      - name: Set up Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Update pyproject.toml\n        run: |\n          python .github/use-local-unidep.py\n      - name: Install example packages\n        run: |\n          set -ex\n          # Loop over all folders in `./example` and install them\n          for d in ./example/*/ ; do\n            pip install -e \"$d\"\n            pkg=$(basename $d)\n            python -c \"import $pkg\"\n            pip list\n          done\n        shell: bash\n\n  micromamba-install:\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [\"3.8\", \"3.9\", \"3.10\", \"3.11\", \"3.12\"] # skips 3.7 (unsupported on GH Actions)\n        platform: [ubuntu-latest, macos-latest, windows-latest]\n    runs-on: ${{ matrix.platform }}\n    env:\n      PYTHONIOENCODING: \"utf8\" # https://gist.github.com/NodeJSmith/e7e37f2d3f162456869f015f842bcf15\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: mamba-org/setup-micromamba@v2\n        with:\n          environment-name: unidep\n          create-args: >-\n            python=${{ matrix.python-version }}\n      - name: Install unidep\n        run: |\n          python -m pip install --upgrade pip\n          pip install -e \".[toml]\"\n        shell: bash -el {0}\n      - name: Update pyproject.toml\n        run: python .github/use-local-unidep.py\n        shell: bash -el {0}\n      - name: Install example packages\n        run: |\n          set -ex\n          # Loop over all folders in `./example` and install them\n          for d in ./example/*/ ; do\n            unidep install -e \"$d\"\n            pkg=$(basename $d)\n            python -c \"import $pkg\"\n            micromamba list\n          done\n        shell: bash -el {0}\n      - name: Install pyproject_toml_project in new environment\n        run: |\n          unidep install -n new-env -e ./example/pyproject_toml_project\n          micromamba activate new-env\n          python -c \"import pyproject_toml_project\"\n        shell: bash -el {0}\n\n\n  miniconda-install:\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [\"3.8\", \"3.12\"] # Just testing the oldest and newest supported versions\n        platform: [ubuntu-latest, macos-latest, windows-latest]\n    runs-on: ${{ matrix.platform }}\n    env:\n      PYTHONIOENCODING: \"utf8\" # https://gist.github.com/NodeJSmith/e7e37f2d3f162456869f015f842bcf15\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n      - uses: conda-incubator/setup-miniconda@v3\n        with:\n          auto-update-conda: true\n          python-version: ${{ matrix.python-version }}\n      - name: Conda info\n        shell: bash -el {0}\n        run: conda info\n      - name: Install unidep\n        run: |\n          python -m pip install --upgrade pip\n          pip install -e \".[toml]\"\n        shell: bash -el {0}\n      - name: Update pyproject.toml\n        run: python .github/use-local-unidep.py\n        shell: bash -el {0}\n      - name: Install example packages\n        run: |\n          set -ex\n          # Loop over all folders in `./example` and install them\n          for d in ./example/*/ ; do\n            unidep install -e \"$d\"\n            pkg=$(basename $d)\n            python -c \"import $pkg\"\n            conda list\n          done\n        shell: bash -el {0}\n      - name: Install pyproject_toml_project in new environment\n        run: |\n          unidep install -n new-env -e ./example/pyproject_toml_project\n          conda activate new-env\n          python -c \"import pyproject_toml_project\"\n        shell: bash -el {0}\n"
  },
  {
    "path": ".github/workflows/pytest.yml",
    "content": "name: pytest\n\non:\n  push:\n    branches: [main]\n  pull_request:\n\njobs:\n  test:\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [\"3.8\", \"3.9\", \"3.10\", \"3.11\", \"3.12\"]  # skips 3.7 (unsupported on GH Actions)\n        platform: [ubuntu-latest, macos-latest, windows-latest]\n    runs-on: ${{ matrix.platform }}\n    env:\n      PYTHONIOENCODING: \"utf8\" # https://gist.github.com/NodeJSmith/e7e37f2d3f162456869f015f842bcf15\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: mamba-org/setup-micromamba@v2\n        with:\n          environment-name: unidep\n          create-args: >-\n            python=${{ matrix.python-version }}\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          if [[ \"${{ matrix.python-version }}\" == \"3.8\" ]]; then\n            # Python 3.8 coverage does not support the \"patch\" config option\n            sed -i.bak '/patch/d' pyproject.toml && rm pyproject.toml.bak\n          fi\n          pip install -e \".[test]\"\n        shell: bash -el {0}\n      - name: Run pytest\n        run: |\n          if [[ \"${{ matrix.platform }}\" == \"ubuntu-latest\" && \"${{ matrix.python-version }}\" == \"3.11\" ]]; then\n            pytest\n          else\n            pytest --cov-fail-under=0\n          fi\n        shell: bash -el {0}\n      - name: Upload coverage to Codecov\n        if: matrix.python-version == '3.11' && matrix.platform == 'ubuntu-latest'\n        uses: codecov/codecov-action@v5\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Upload Python Package\n\non:\n  release:\n    types: [published]\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    environment:\n      name: pypi\n      url: https://pypi.org/p/${{ github.repository }}\n    permissions:\n      id-token: write\n    steps:\n      - uses: actions/checkout@v6\n      - name: Set up Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: \"3.14.2\"\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install setuptools wheel build\n      - name: Build\n        run: |\n          python -m build\n      - name: Publish package distributions to PyPI\n        uses: pypa/gh-action-pypi-publish@release/v1\n"
  },
  {
    "path": ".github/workflows/toc.yaml",
    "content": "on: push\nname: TOC Generator\njobs:\n  generateTOC:\n    name: TOC Generator\n    runs-on: ubuntu-latest\n    steps:\n      - uses: technote-space/toc-generator@v4\n        with:\n          TOC_TITLE: \"\"\n          TARGET_PATHS: \"README.md,example/README.md\"\n"
  },
  {
    "path": ".github/workflows/update-readme.yml",
    "content": "name: Update README.md\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n\njobs:\n  update_readme:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out repository\n        uses: actions/checkout@v6\n        with:\n          persist-credentials: false\n          fetch-depth: 0\n\n      - name: Set up Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: '3.14.2'\n\n      - name: Install Python dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install markdown-code-runner\n          pip install -e .\n\n      - name: Run markdown-code-runner\n        run: |\n          markdown-code-runner README.md\n          cd example\n          markdown-code-runner README.md\n\n      - name: Commit updated files\n        id: commit\n        run: |\n          git add -u .\n          git config --local user.email \"github-actions[bot]@users.noreply.github.com\"\n          git config --local user.name \"github-actions[bot]\"\n          if git diff --quiet && git diff --staged --quiet; then\n            echo \"No changes, skipping commit.\"\n            echo \"commit_status=skipped\" >> $GITHUB_ENV\n          else\n            git commit -m \"Update files from markdown-code-runner\"\n            echo \"commit_status=committed\" >> $GITHUB_ENV\n          fi\n\n      - name: Push changes\n        if: env.commit_status == 'committed'\n        uses: ad-m/github-push-action@v1.1.0\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          branch: ${{ github.head_ref }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*,cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# IPython Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# dotenv\n.env\n\n# virtualenv\nvenv/\nENV/\n\n# Spyder project settings\n.spyderproject\n\n# Rope project settings\n.ropeproject\n\n# other\n.pixi\n.DS_Store\n*.code-workspace\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v5.0.0\n    hooks:\n      - id: check-added-large-files\n      - id: trailing-whitespace\n      - id: end-of-file-fixer\n      - id: mixed-line-ending\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: \"v0.9.9\"\n    hooks:\n      - id: ruff\n        args: [\"--fix\"]\n      - id: ruff-format\n  - repo: https://github.com/pre-commit/mirrors-mypy\n    rev: \"v1.15.0\"\n    hooks:\n      - id: mypy\n        additional_dependencies: [\"types-PyYAML\", \"types-setuptools\"]\n"
  },
  {
    "path": ".pre-commit-hooks.yaml",
    "content": "- id: unidep-environment-yaml\n  name: unidep environment.yaml\n  description: Generate environment.yaml from requirements.yaml using unidep.\n  entry: unidep merge\n  language: python\n  files: '(requirements\\.yaml|pyproject\\.toml)$'\n  pass_filenames: false\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "version: 2\n\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.12\"\n\nsphinx:\n  configuration: docs/source/conf.py\n\npython:\n  install:\n    - method: pip\n      path: .\n      extra_requirements:\n        - docs\n"
  },
  {
    "path": "LICENSE",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2023, Bas Nijholt\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "# 🚀 UniDep - Unified Conda and Pip Dependency Management 🚀\n\n![UniDep logo](https://media.githubusercontent.com/media/basnijholt/nijho.lt/main/content/project/unidep/featured.png)\n\n[![PyPI](https://img.shields.io/pypi/v/unidep.svg)](https://pypi.python.org/pypi/unidep)\n[![Build Status](https://github.com/basnijholt/unidep/actions/workflows/pytest.yml/badge.svg)](https://github.com/basnijholt/unidep/actions/workflows/pytest.yml)\n[![CodeCov](https://codecov.io/gh/basnijholt/unidep/branch/main/graph/badge.svg)](https://codecov.io/gh/basnijholt/unidep)\n[![GitHub Repo stars](https://img.shields.io/github/stars/basnijholt/unidep)](https://github.com/basnijholt/unidep)\n[![Documentation](https://readthedocs.org/projects/unidep/badge/?version=latest)](https://unidep.readthedocs.io/)\n[![Python Bytes](https://img.shields.io/badge/Python_Bytes-366-D7F9FF?logo=applepodcasts&labelColor=blue)](https://www.youtube.com/live/PRaTs3PnJvI?si=UrVozo81Pj8WcyXh&t=489)\n\n> UniDep streamlines Python project dependency management by unifying Conda and Pip packages in a single system.\n> [Learn when to use UniDep](#q-when-to-use-unidep) in our [FAQ](#-faq).\n\nHandling dependencies in Python projects can be challenging, especially when juggling Python and non-Python packages.\nThis often leads to confusion and inefficiency, as developers juggle between multiple dependency files.\n\n- **📝 Unified Dependency File**: Use either `requirements.yaml` or `pyproject.toml` to manage both Conda and Pip dependencies in one place.\n- **⚙️ Build System Integration**: Integrates with Setuptools and Hatchling for automatic dependency handling during `pip install ./your-package`.\n- **💻 One-Command Installation**: `unidep install` handles Conda, Pip, and local dependencies effortlessly.\n- **⚡️ Fast Pip Operations**: Leverages `uv` (if installed) for faster pip installations.\n- **🏢 Monorepo-Friendly**: Render (multiple) `requirements.yaml` or `pyproject.toml` files into one Conda `environment.yaml` file and maintain fully consistent global *and* per sub package `conda-lock` files.\n- **🌍 Platform-Specific Support**: Specify dependencies for different operating systems or architectures.\n- **🔧 `pip-compile` Integration**: Generate fully pinned `requirements.txt` files from `requirements.yaml` or `pyproject.toml` files using `pip-compile`.\n- **🔒 Integration with `conda-lock`**: Generate fully pinned `conda-lock.yml` files from (multiple) `requirements.yaml` or `pyproject.toml` file(s), leveraging `conda-lock`.\n- **🥧 Pixi Support**: Generate `pixi.toml` files from your dependency files, enabling Pixi-based workflows while keeping UniDep as the single source of truth.\n- **🤓 Nerd stats**: written in Python, 100% test coverage, fully-typed, all Ruff's rules enabled, easily extensible, and minimal dependencies\n\n`unidep` is designed to make dependency management in Python projects as simple and efficient as possible.\nTry it now and streamline your development process!\n\n> [!TIP]\n> Check out the [example `requirements.yaml` and `pyproject.toml` below](#example).\n\n<!-- toc-start -->\n\n## :books: Table of Contents\n\n<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->\n\n- [:rocket: Bootstrap from Scratch](#rocket-bootstrap-from-scratch)\n- [:package: Installation](#package-installation)\n- [:memo: `requirements.yaml` and `pyproject.toml` structure](#memo-requirementsyaml-and-pyprojecttoml-structure)\n  - [Example](#example)\n    - [Example `requirements.yaml`](#example-requirementsyaml)\n    - [Example `pyproject.toml`](#example-pyprojecttoml)\n  - [Key Points](#key-points)\n  - [Supported Version Pinnings](#supported-version-pinnings)\n  - [Conflict Resolution](#conflict-resolution)\n    - [How It Works](#how-it-works)\n  - [Platform Selectors](#platform-selectors)\n    - [Supported Selectors](#supported-selectors)\n    - [Usage](#usage)\n    - [Implementation](#implementation)\n  - [Custom Pip Index URLs](#custom-pip-index-urls)\n    - [How It Works](#how-it-works-1)\n    - [Example Usage](#example-usage)\n    - [Generated Output](#generated-output)\n  - [`[project.dependencies]` in `pyproject.toml` handling](#projectdependencies-in-pyprojecttoml-handling)\n- [:jigsaw: Build System Integration](#jigsaw-build-system-integration)\n  - [Local Dependencies in Monorepos](#local-dependencies-in-monorepos)\n  - [PyPI Alternatives for Local Dependencies](#pypi-alternatives-for-local-dependencies)\n  - [Overriding Nested Vendor Copies with `use`](#overriding-nested-vendor-copies-with-use)\n    - [Example: Override foo's bundled bar with your PyPI build](#example-override-foos-bundled-bar-with-your-pypi-build)\n  - [All `use` values](#all-use-values)\n  - [Build System Behavior](#build-system-behavior)\n  - [Example packages](#example-packages)\n  - [Setuptools Integration](#setuptools-integration)\n  - [Hatchling Integration](#hatchling-integration)\n- [:desktop_computer: As a CLI](#desktop_computer-as-a-cli)\n  - [`unidep merge`](#unidep-merge)\n  - [`unidep install`](#unidep-install)\n  - [`unidep install-all`](#unidep-install-all)\n  - [`unidep conda-lock`](#unidep-conda-lock)\n  - [`unidep pixi`](#unidep-pixi)\n    - [What `unidep pixi` generates](#what-unidep-pixi-generates)\n    - [Dependency reconciliation rules (important)](#dependency-reconciliation-rules-important)\n    - [Channels/platforms precedence](#channelsplatforms-precedence)\n    - [Example (single-file)](#example-single-file)\n  - [`unidep pip-compile`](#unidep-pip-compile)\n  - [`unidep pip`](#unidep-pip)\n  - [`unidep conda`](#unidep-conda)\n- [❓ FAQ](#-faq)\n  - [**Q: When to use UniDep?**](#q-when-to-use-unidep)\n  - [**Q: Just show me a full example!**](#q-just-show-me-a-full-example)\n  - [**Q: Uses of UniDep in the wild?**](#q-uses-of-unidep-in-the-wild)\n  - [**Q: How do I force PyPI instead of a local path for one dependency?**](#q-how-do-i-force-pypi-instead-of-a-local-path-for-one-dependency)\n  - [**Q: How do I ignore a local dependency entirely?**](#q-how-do-i-ignore-a-local-dependency-entirely)\n  - [**Q: A submodule brings its own copy of package X. How do I avoid conflicts?**](#q-a-submodule-brings-its-own-copy-of-package-x-how-do-i-avoid-conflicts)\n  - [**Q: How is this different from conda/mamba/pip?**](#q-how-is-this-different-from-condamambapip)\n  - [**Q: I found a project using unidep, now what?**](#q-i-found-a-project-using-unidep-now-what)\n  - [**Q: How to handle local dependencies that do not use UniDep?**](#q-how-to-handle-local-dependencies-that-do-not-use-unidep)\n  - [**Q: Can't Conda already do this?**](#q-cant-conda-already-do-this)\n  - [**Q: What is the difference between `conda-lock` and `unidep conda-lock`?**](#q-what-is-the-difference-between-conda-lock-and-unidep-conda-lock)\n  - [**Q: What is the difference between `hatch-conda` / `pdm-conda` and `unidep`?**](#q-what-is-the-difference-between-hatch-conda--pdm-conda-and-unidep)\n- [:hammer_and_wrench: Troubleshooting](#hammer_and_wrench-troubleshooting)\n  - [`pip install` fails with `FileNotFoundError`](#pip-install-fails-with-filenotfounderror)\n- [:warning: Limitations](#warning-limitations)\n\n<!-- END doctoc generated TOC please keep comment here to allow auto update -->\n\n<!-- toc-end -->\n\n## :rocket: Bootstrap from Scratch\n\nTo get started quickly with UniDep, run the following command. This will download and install [micromamba](https://mamba.readthedocs.io/en/latest/installation/micromamba-installation.html) (recommended for fast Conda environment management), [uv](https://docs.astral.sh/uv/getting-started/installation/) (recommended for faster pip installations), and then install UniDep:\n\n```bash\n\"${SHELL}\" <(curl -LsSf raw.githubusercontent.com/basnijholt/unidep/main/bootstrap.sh)\n```\n\n> [!NOTE]\n> Micromamba and uv are recommended to optimize your installation experience, but they are not required if you prefer to use your existing Conda and pip setup.\n\n> [!WARNING]\n> NEVER! run scripts from the internet without understanding what they do. Always inspect the script first!\n\n<details>\n<summary>Pin the hash of the bootstrap script with:</summary>\n\n<!-- CODE:BASH:START -->\n<!-- HASH=$(git log -n 1 --pretty=format:\"%H\" -- bootstrap.sh) -->\n<!-- echo '```bash' -->\n<!-- echo '\"${SHELL}\"' '<(curl -LsSf raw.githubusercontent.com/basnijholt/unidep/'\"$HASH\"'/bootstrap.sh)' -->\n<!-- echo '```' -->\n<!-- CODE:END -->\n<!-- OUTPUT:START -->\n<!-- ⚠️ This content is auto-generated by `markdown-code-runner`. -->\n```bash\n\"${SHELL}\" <(curl -LsSf raw.githubusercontent.com/basnijholt/unidep/939246571b65004391c425eb6df713303663054a/bootstrap.sh)\n```\n\n<!-- OUTPUT:END -->\n\n</details>\n\n## :package: Installation\n\nTo install `unidep`, run one of the following commands that use [`pipx`](https://pipx.pypa.io/) (recommended), `pip`, or `conda`:\n\n```bash\npipx install \"unidep[all]\"  # Recommended (install as a standalone CLI)\n```\n\nor\n\n```bash\npip install \"unidep[all]\"\n```\n\nor\n\n```bash\nconda install -c conda-forge unidep\n```\n\n## :memo: `requirements.yaml` and `pyproject.toml` structure\n\n`unidep` allows either using a\n1. `requirements.yaml` file with a specific format (similar but _**not**_ the same as a Conda `environment.yaml` file) or\n2. `pyproject.toml` file with a `[tool.unidep]` section.\n\nBoth files contain the following keys:\n\n- **name** (Optional): For documentation, not used in the output.\n- **channels**: List of conda channels for packages, such as `conda-forge`.\n- **dependencies**: Mix of Conda and Pip packages.\n- **local_dependencies** (Optional): List of paths to other `requirements.yaml` or `pyproject.toml` files to include.\n- **optional_dependencies** (Optional): Dictionary with lists of optional dependencies.\n- **platforms** (Optional): List of platforms that are supported (used in `conda-lock`).\n- **pip_indices** (Optional): List of custom pip index URLs for private or alternative package repositories.\n\nWhether you use a `requirements.yaml` or `pyproject.toml` file, the same information can be specified in either.\nChoose the format that works best for your project.\n\n### Example\n\n#### Example `requirements.yaml`\n\nExample of a `requirements.yaml` file:\n\n```yaml\nname: example_environment\nchannels:\n  - conda-forge\ndependencies:\n  - numpy                   # same name on conda and pip\n  - conda: python-graphviz  # When names differ between Conda and Pip\n    pip: graphviz\n  - pip: slurm-usage >=1.1.0,<2  # pip-only\n  - conda: mumps                 # conda-only\n  # Use platform selectors\n  - conda: cuda-toolkit =11.8    # [linux64]\nlocal_dependencies:\n  - ../other-project-using-unidep     # include other projects that use unidep\n  - ../common-requirements.yaml       # include other requirements.yaml files\n  - ../project-not-managed-by-unidep  # 🚨 Skips its dependencies!\noptional_dependencies:\n  test:\n    - pytest\n  full:\n    - ../other-local-dep[test]  # include its optional 'test' dependencies\nplatforms:  # (Optional) specify platforms that are supported (used in conda-lock)\n  - linux-64\n  - osx-arm64\npip_indices:  # (Optional) additional pip index URLs for private packages\n  - https://pypi.org/simple/  # Main PyPI index (automatically included if not specified)\n  - https://private.company.com/simple/  # Private company index\n  - https://${PIP_USER}:${PIP_PASSWORD}@private.pypi.org/simple/  # Authenticated index with env vars\n```\n\n> [!IMPORTANT]\n> `unidep` can process this during `pip install` and create a Conda installable `environment.yaml` or `conda-lock.yml` file, and more!\n\n> [!NOTE]\n> For a more in-depth example containing multiple installable projects, see the [`example`](example/) directory.\n\n#### Example `pyproject.toml`\n\n***Alternatively***, one can fully configure the dependencies in the `pyproject.toml` file in the `[tool.unidep]` section:\n\n```toml\n[tool.unidep]\nchannels = [\"conda-forge\"]\ndependencies = [\n    \"numpy\",                                         # same name on conda and pip\n    { conda = \"python-graphviz\", pip = \"graphviz\" }, # When names differ between Conda and Pip\n    { pip = \"slurm-usage >=1.1.0,<2\" },              # pip-only\n    { conda = \"mumps\" },                             # conda-only\n    { conda = \"cuda-toolkit =11.8:linux64\" }         # Use platform selectors by appending `:linux64`\n]\nlocal_dependencies = [\n    \"../other-project-using-unidep\",    # include other projects that use unidep\n    \"../common-requirements.yaml\",      # include other requirements.yaml files\n    \"../project-not-managed-by-unidep\"  # 🚨 Skips its dependencies!\n]\noptional_dependencies = {\n    test = [\"pytest\"],\n    full = [\"../other-local-dep[test]\"]  # include its optional 'test' dependencies\n}\nplatforms = [ # (Optional) specify platforms that are supported (used in conda-lock)\n    \"linux-64\",\n    \"osx-arm64\"\n]\npip_indices = [ # (Optional) additional pip index URLs for private packages\n    \"https://pypi.org/simple/\",  # Main PyPI index (automatically included if not specified)\n    \"https://private.company.com/simple/\",  # Private company index\n    \"https://${PIP_USER}:${PIP_PASSWORD}@private.pypi.org/simple/\"  # Authenticated index with env vars\n]\n```\n\nThis data structure is *identical* to the `requirements.yaml` format, with the exception of the `name` field and the [platform selectors](#platform-selectors).\nIn the `requirements.yaml` file, one can use e.g., `# [linux64]`, which in the `pyproject.toml` file is `:linux64` at the end of the package name.\n\nSee [Build System Integration](#jigsaw-build-system-integration) for more information on how to set up `unidep` with different build systems (Setuptools or Hatchling).\n\n> [!IMPORTANT]\n> In these docs, we often mention the `requirements.yaml` format for simplicity, but the same information can be specified in `pyproject.toml` as well.\n> Everything that is possible in `requirements.yaml` is also possible in `pyproject.toml`!\n\n### Key Points\n\n- Standard names (e.g., `- numpy`) are assumed to be the same for Conda and Pip.\n- Use a dictionary with `conda: <package>` *and* `pip: <package>` to specify different names across platforms.\n- Use `pip:` to specify packages that are only available through Pip.\n- Use `conda:` to specify packages that are only available through Conda.\n- Use `# [selector]` (YAML only) or `package:selector` to specify platform-specific dependencies.\n- Use `local_dependencies:` to include other `requirements.yaml` or `pyproject.toml` files and merge them into one. Also allows projects that are not managed by `unidep` to be included, but be aware that this skips their dependencies! Can specify PyPI alternatives for monorepo setups (see [PyPI Alternatives for Local Dependencies](#pypi-alternatives-for-local-dependencies)).\n- Use `optional_dependencies:` to specify optional dependencies. Can be installed like `unidep install \".[test]\"` or `pip install \".[test]\"`.\n- Use `platforms:` to specify the platforms that are supported. If omitted, all platforms are assumed to be supported.\n- Use `pip_indices:` to specify additional pip index URLs for installing packages from private or alternative package repositories (see [Custom Pip Index URLs](#custom-pip-index-urls) below).\n\n> *We use the YAML notation here, but the same information can be specified in `pyproject.toml` as well.*\n\n### Supported Version Pinnings\n\nUniDep has two relevant pinning layers:\n\n- **Dict-based conflict helper (`unidep._conflicts.resolve_conflicts`)**: combines repeated pinnings with the Conda-compatible subset of operators: `=`, `>`, `<`, `>=`, `<=`, `!=`.\n- **CLI-facing pip renderers**: additionally preserve safe pip-only PEP 440 forms such as `==` and `~=` when those constraints can be kept explicitly without ambiguity.\n\nExamples:\n\n- Conda-compatible merge: `>1.0.0, <2.0.0`\n- Exact pip pin: `==0.25.2.1`\n- Compatible release pin: `~=1.0`\n\n- **Redundant Pinning Resolution**: Automatically resolves redundant compatible constraints when possible.\n  - Example: `>1.0.0, >0.5.0` simplifies to `>1.0.0`.\n\n- **Contradictory Version Detection**: Errors are raised for contradictory pinnings to maintain dependency integrity. See the [Conflict Resolution](#conflict-resolution) section for more information.\n  - Example: Specifying `>2.0.0, <1.5.0` triggers a `VersionConflictError`.\n\n- **Invalid Pinning Detection**: Detects and raises errors for unrecognized or improperly formatted version specifications.\n\n- **Conda Build Pinning**: UniDep also supports Conda's build pinning, allowing you to specify builds in your pinning patterns.\n  - Example: Conda supports pinning builds like `qsimcirq * cuda*` or `vtk * *egl*`.\n  - **Limitation**: While UniDep allows such build pinning, it requires that there be a single pin per package. UniDep cannot resolve conflicts where multiple build pinnings are specified for the same package.\n    - Example: UniDep can handle `qsimcirq * cuda*`, but it cannot resolve a scenario with both `qsimcirq * cuda*` and `qsimcirq * cpu*`.\n\n- **Other Special Cases**: In addition to Conda build pins, UniDep supports all special pinning formats, such as VCS (Version Control System) URLs or local file paths. This includes formats like `package @ git+https://git/repo/here` or `package @ file:///path/to/package`. However, UniDep has a limitation: it can handle only one special pin per package. These special pins can be combined with an unpinned version specification, but not with multiple special pin formats for the same package.\n  - Example: UniDep can manage dependencies specified as `package @ git+https://git/repo/here` and `package` in the same `requirements.yaml`. However, it cannot resolve scenarios where both `package @ git+https://git/repo/here` and `package @ file:///path/to/package` are specified for the same package.\n\n> [!WARNING]\n> **Pinning Validation and Combination**: UniDep actively validates and/or combines pinnings only when **multiple different pinnings** are specified for the same package.\n> This means if your `requirements.yaml` files include multiple pinnings for a single package, UniDep will attempt to resolve them into a single, coherent specification.\n> However, if the pinnings are contradictory or incompatible, UniDep will raise an error to alert you of the conflict.\n\n### Conflict Resolution\n\n`unidep` features a conflict resolution mechanism to manage version conflicts and platform-specific dependencies in `requirements.yaml` or `pyproject.toml` files.\n\n#### How It Works\n\n- **Within-source pinning priority**: `unidep` combines repeated entries within the same source (`conda` or `pip`) and gives priority to version-pinned packages. For instance, if both `foo` and `foo <1` are listed for the same source, `foo <1` is selected due to its specific version pin.\n\n- **Entry-based rendering**: CLI-facing outputs now work from `parse_requirements(...).dependency_entries`, preserving each original declaration long enough for the shared selector to choose the final Conda-like or pip-only result.\n\n- **Lower-level metadata helper**: `unidep._conflicts.resolve_conflicts()` still exists for the older dict-based requirements model (`ParsedRequirements.requirements`), but it is no longer the main renderer handoff.\n\n- **Conda-like paired-entry selection**: For explicit dependency entries that provide both `conda:` and `pip:` alternatives, Conda-like outputs use deterministic source selection rules: Pip extras win, otherwise a single pinned side wins, and ties prefer Conda.\n\n- **Pip-only output selection**: Pip-only exports (`unidep pip`, setuptools integration, `get_python_dependencies`) keep the Pip dependency when it exists, even if Conda would win for a Conda-like output.\n\n- **Platform-Specific Version Pinning**: `unidep` resolves platform-specific dependency conflicts by preferring the version with the narrowest platform scope. For instance, given `foo <3 # [linux64]` and `foo >1`, it installs `foo >1,<3` exclusively on Linux-64 and `foo >1` on all other platforms.\n\n- **Intractable Conflicts**: When conflicts are irreconcilable within a source (e.g., `foo >1` vs. `foo <1`), `unidep` raises an exception.\n\n### Platform Selectors\n\nThis tool supports a range of platform selectors that allow for specific handling of dependencies based on the user's operating system and architecture. This feature is particularly useful for managing conditional dependencies in diverse environments.\n\n#### Supported Selectors\n\nThe following selectors are supported:\n\n- `linux`: For all Linux-based systems.\n- `linux64`: Specifically for 64-bit Linux systems.\n- `aarch64`: For Linux systems on ARM64 architectures.\n- `ppc64le`: For Linux on PowerPC 64-bit Little Endian architectures.\n- `osx`: For all macOS systems.\n- `osx64`: Specifically for 64-bit macOS systems.\n- `arm64`: For macOS systems on ARM64 architectures (Apple Silicon).\n- `macos`: An alternative to `osx` for macOS systems.\n- `unix`: A general selector for all UNIX-like systems (includes Linux and macOS).\n- `win`: For all Windows systems.\n- `win64`: Specifically for 64-bit Windows systems.\n\n#### Usage\n\nSelectors are used in `requirements.yaml` files to conditionally include dependencies based on the platform:\n\n```yaml\ndependencies:\n  - some-package >=1  # [unix]\n  - another-package   # [win]\n  - special-package   # [osx64]\n  - pip: cirq         # [macos win]\n    conda: cirq       # [linux]\n```\n\nOr when using `pyproject.toml` instead of `requirements.yaml`:\n\n```toml\n[tool.unidep]\ndependencies = [\n    \"some-package >=1:unix\",\n    \"another-package:win\",\n    \"special-package:osx64\",\n    { pip = \"cirq:macos win\", conda = \"cirq:linux\" },\n]\n```\n\nIn this example:\n\n- `some-package` is included only in UNIX-like environments (Linux and macOS).\n- `another-package` is specific to Windows.\n- `special-package` is included only for 64-bit macOS systems.\n- `cirq` is managed by `pip` on macOS and Windows, and by `conda` on Linux. This demonstrates how you can specify different package managers for the same package based on the platform.\n\nNote that the `package-name:unix` syntax can also be used in the `requirements.yaml` file, but the `package-name # [unix]` syntax is not supported in `pyproject.toml`.\n\n#### Implementation\n\n`unidep` parses these selectors and filters dependencies according to the platform where it's being installed.\nIt is also used for creating environment and lock files that are portable across different platforms, ensuring that each environment has the appropriate dependencies installed.\n\n### Custom Pip Index URLs\n\nThe `pip_indices` field allows you to specify additional pip index URLs for installing packages from private or alternative package repositories. It may be given as a single string or a list of strings. This is particularly useful for:\n\n- **Private Company Packages**: Access internal packages hosted on private PyPI servers\n- **Alternative Package Repositories**: Use mirrors or alternative package sources\n- **Authenticated Repositories**: Access protected repositories using environment variables for credentials\n\n#### How It Works\n\nWhen `pip_indices` is specified:\n\n1. **First index is primary**: The first URL in the list is used as `--index-url` (primary index)\n2. **Additional indices are extra**: Subsequent URLs are passed as `--extra-index-url` flags\n3. **Environment variable expansion**: Variables like `${PIP_USER}` and `${PIP_PASSWORD}` are automatically expanded from environment variables\n4. **Automatic deduplication**: Duplicate URLs are automatically removed while preserving order\n5. **Integration with all tools**: Works with `unidep install`, `pip install`, and when using `uv` as the installer\n\n#### Example Usage\n\n```yaml\n# requirements.yaml\npip_indices:\n  - https://pypi.org/simple/  # Primary index (optional, used by default)\n  - https://test.pypi.org/simple/  # Test PyPI for pre-release packages\n  - https://${GITLAB_USER}:${GITLAB_TOKEN}@gitlab.company.com/api/v4/projects/123/packages/pypi/simple  # Private GitLab\n```\n\n```toml\n# pyproject.toml\n[tool.unidep]\npip_indices = [\n    \"https://download.pytorch.org/whl/cpu\",  # PyTorch CPU-only builds\n    \"https://${ARTIFACTORY_USER}:${ARTIFACTORY_PASSWORD}@artifactory.company.com/pypi/simple\"  # Artifactory\n]\n```\n\n#### Generated Output\n\nWhen generating `environment.yaml` files, `pip_indices` are included as `pip-repositories`:\n\n```yaml\n# Generated environment.yaml\nname: myproject\nchannels:\n  - conda-forge\npip-repositories:\n  - https://pypi.org/simple/\n  - https://private.company.com/simple/\ndependencies:\n  - python\n  - pip:\n    - private-package  # Will be installed from the private index\n```\n\n\n> [!TIP]\n> Store sensitive credentials in environment variables rather than hardcoding them in configuration files. UniDep automatically expands `${VAR_NAME}` patterns.\n\n### `[project.dependencies]` in `pyproject.toml` handling\n\nThe `project_dependency_handling` option in `[tool.unidep]` (in `pyproject.toml`) controls how dependencies listed in the standard `[project.dependencies]` section of `pyproject.toml` are handled when processed by `unidep`.\n\n**Modes:**\n\n- **`ignore`** (default): Dependencies in `[project.dependencies]` are ignored by `unidep`.\n- **`same-name`**: Dependencies in `[project.dependencies]` are treated as dependencies with the same name for both Conda and Pip. They will be added to the `dependencies` list in `[tool.unidep]` under the assumption that the package name is the same for both package managers.\n- **`pip-only`**: Dependencies in `[project.dependencies]` are treated as pip-only dependencies. They will be added to the `dependencies` list in `[tool.unidep]` under the `pip` key.\n\n**Example `pyproject.toml`:**\n\n```toml\n[build-system]\nrequires = [\"hatchling\", \"unidep\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\nname = \"my-project\"\nversion = \"0.1.0\"\ndependencies = [  # These will be handled according to the `project_dependency_handling` option\n  \"requests\",\n  \"pandas\",\n]\n\n[tool.unidep]\nproject_dependency_handling = \"same-name\"  # Or \"pip-only\", \"ignore\"\ndependencies = [\n    {conda = \"python-graphviz\", pip = \"graphivz\"},\n]\n```\n\n**Notes:**\n\n- The `project_dependency_handling` option only affects how dependencies from `[project.dependencies]` are processed. Dependencies directly listed under `[tool.unidep.dependencies]` are handled as before.\n- This feature is helpful for projects that are already using the standard `[project.dependencies]` field and want to integrate `unidep` without duplicating their dependency list.\n- The `project_dependency_handling` feature is _*only available*_ when using `pyproject.toml` files. It is not supported in `requirements.yaml` files.\n\n## :jigsaw: Build System Integration\n\n> [!TIP]\n> See [`example/`](example/) for working examples of using `unidep` with different build systems.\n\n`unidep` seamlessly integrates with popular Python build systems to simplify dependency management in your projects.\n\n### Local Dependencies in Monorepos\n\nLocal dependencies are essential for monorepos and multi-package projects, allowing you to:\n- Share code between packages during development\n- Maintain separate releases for each package\n- Test changes across multiple packages simultaneously\n\nHowever, when building wheels for distribution, local paths create non-portable packages that only work on the original system.\n\n### PyPI Alternatives for Local Dependencies\n\nUniDep solves this problem by letting you specify both local paths (for development) and PyPI packages (for distribution):\n\n```yaml\n# requirements.yaml\ndependencies:\n  - numpy\n  - pandas\n\nlocal_dependencies:\n  # Standard string format for local dependencies\n  - ../shared-lib\n\n  # Dictionary format with optional PyPI alternative for build-time\n  - local: ../auth-lib\n    pypi: company-auth-lib>=1.0\n\n  - local: ../utils\n    pypi: company-utils~=2.0\n    use: pypi  # see [Overriding Nested Vendor Copies](#overriding-nested-vendor-copies-with-use)\n```\n\nOr in `pyproject.toml`:\n\n```toml\n[tool.unidep]\ndependencies = [\"numpy\", \"pandas\"]\n\nlocal_dependencies = [\n    # Standard string format for local dependencies\n    \"../shared-lib\",\n\n    # Dictionary format with optional PyPI alternative for build-time\n    {local = \"../auth-lib\", pypi = \"company-auth-lib>=1.0\"},\n    {local = \"../utils\", pypi = \"company-utils~=2.0\", use = \"pypi\"},\n]\n```\n\n**How it works:**\n- **During development** (e.g., `unidep install` or `pip install -e .`): Uses local paths when they exist\n- **When building wheels**: PyPI alternatives (if specified) are used to create portable packages\n- The standard string format continues to work as always for local dependencies\n\n> [!TIP]\n> PyPI alternatives ensure your wheels are portable and can be installed anywhere, not just on the build system. Use the `use` field (see [Overriding Nested Vendor Copies](#overriding-nested-vendor-copies-with-use)) to control whether UniDep installs the local path, forces PyPI, or skips the entry entirely.\n\n### Overriding Nested Vendor Copies with `use`\n\n**The Problem:** When vendoring dependencies as git submodules, you often encounter conflicts where a submodule bundles its own copy of a dependency you also use, but at a different version.\n\n**The Solution:** Use `use: pypi` to force your PyPI package instead of the vendored copy, with automatic propagation to all nested references.\n\n#### Example: Override foo's bundled bar with your PyPI build\n\nYour project vendors `foo` as a submodule. Foo bundles `bar@1.0`, but you need `bar@2.0`:\n\n```\nproject/\n  third_party/\n    foo/                    # git submodule you don't control\n      third_party/\n        bar/                # foo bundles bar@1.0\n```\n\n**Solution with `use: pypi`:**\n\n```yaml\nlocal_dependencies:\n  - ./third_party/foo       # Keep foo editable for development\n\n  # Override: force YOUR PyPI build of bar\n  - local: ./third_party/foo/third_party/bar\n    pypi: my-bar>=2.0\n    use: pypi               # Install from PyPI, skip local path\n```\n\n**What happens:**\n1. `foo` stays local (editable for development)\n2. `my-bar>=2.0` gets installed from PyPI (not foo's bundled v1.0)\n3. **Propagates**: Every nested reference to `bar` uses your PyPI package\n4. Works with `unidep install`, `unidep conda-lock`, all CLI commands\n\nThis is the **key difference** from just using `pypi:` as a build-time fallback - `use: pypi` **forces the PyPI package during development** while keeping other local dependencies editable.\n\n---\n\n### All `use` values\n\nTell UniDep what to **use** for each entry in `local_dependencies`:\n\n| `use` value | When to use | Installs from | Propagates override? |\n|------------|-------------|---------------|---------------------|\n| `local` *(default)* | Normal local development | Local path | - |\n| `pypi` | **Force PyPI** even when local exists | `pypi:` spec | Yes |\n| `skip` | Ignore this path entirely | Nothing | Yes |\n\n**Common patterns:**\n\n```yaml\nlocal_dependencies:\n  # Standard local development (default)\n  - ../shared-lib\n\n  # Force PyPI to override nested vendor copy\n  - local: ./vendor/foo/nested/bar\n    pypi: my-bar>=2.0\n    use: pypi\n\n  # Skip a path without installing anything\n  - local: ./deprecated-module\n    use: skip\n```\n\n> [!NOTE]\n> **Precedence:** The `use` flag on the entry itself always wins. When UniDep encounters the same path in nested `local_dependencies`, it uses your override. Setting `UNIDEP_SKIP_LOCAL_DEPS=1` forces any effective `use: local` to behave like `pypi` (if specified) or `skip`, but does **not** override explicit `use: pypi` or `use: skip`.\n\n> [!WARNING]\n> If `use: pypi` is set but no `pypi:` requirement is provided, UniDep exits with a clear error so you can supply the missing spec.\n\n### Build System Behavior\n\n**Important differences between build backends:**\n- **Setuptools**: Builds wheels containing `file://` URLs with absolute paths. These wheels only work on the original system.\n- **Hatchling**: Rejects `file://` URLs by default, preventing non-portable wheels.\n\nTo ensure portable wheels, you can use the `UNIDEP_SKIP_LOCAL_DEPS` environment variable:\n\n```bash\n# Force use of PyPI alternatives even when local paths exist\nUNIDEP_SKIP_LOCAL_DEPS=1 python -m build\n\n# For hatch projects\nUNIDEP_SKIP_LOCAL_DEPS=1 hatch build\n\n# For uv build\nUNIDEP_SKIP_LOCAL_DEPS=1 uv build\n```\n\n> [!NOTE]\n> **When `UNIDEP_SKIP_LOCAL_DEPS=1` is set:**\n> - Any effective `use: local` behaves as `use: pypi` (if a `pypi` spec exists) or `use: skip`\n> - Explicit `use: pypi` and `use: skip` remain unchanged\n> - Dependencies from local packages are still included (from their `requirements.yaml`/`pyproject.toml`)\n\n### Example packages\n\nExplore these installable [example](example/) packages to understand how `unidep` integrates with different build tools and configurations:\n\n| Project                                                    | Build Tool   | `pyproject.toml` | `requirements.yaml` | `setup.py` |\n| ---------------------------------------------------------- | ------------ | ---------------- | ------------------- | ---------- |\n| [`setup_py_project`](example/setup_py_project)             | `setuptools` | ✅                | ✅                   | ✅          |\n| [`setuptools_project`](example/setuptools_project)         | `setuptools` | ✅                | ✅                   | ❌          |\n| [`pyproject_toml_project`](example/pyproject_toml_project) | `setuptools` | ✅                | ❌                   | ❌          |\n| [`hatch_project`](example/hatch_project)                   | `hatch`      | ✅                | ✅                   | ❌          |\n| [`hatch2_project`](example/hatch2_project)                 | `hatch`      | ✅                | ❌                   | ❌          |\n\n### Setuptools Integration\n\nFor projects using `setuptools`, configure `unidep` in `pyproject.toml` and either specify dependencies in a `requirements.yaml` file or include them in `pyproject.toml` too.\n\n- **Using `pyproject.toml` only**: The `[project.dependencies]` field in `pyproject.toml` gets automatically populated from `requirements.yaml` or from the `[tool.unidep]` section in `pyproject.toml`.\n- **Using `setup.py`**: The `install_requires` field in `setup.py` automatically reflects dependencies specified in `requirements.yaml` or `pyproject.toml`.\n\n**Example `pyproject.toml` Configuration**:\n\n```toml\n[build-system]\nbuild-backend = \"setuptools.build_meta\"\nrequires = [\"setuptools\", \"unidep\"]\n\n[project]\ndynamic = [\"dependencies\"]\n```\n\n### Hatchling Integration\n\nFor projects managed with [Hatch](https://hatch.pypa.io/), `unidep` can be configured in `pyproject.toml` to automatically process the dependencies from `requirements.yaml` or from the `[tool.unidep]` section in `pyproject.toml`.\n\n**Example Configuration for Hatch**:\n\n```toml\n[build-system]\nrequires = [\"hatchling\", \"unidep\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\ndynamic = [\"dependencies\"]\n# Additional project configurations\n\n[tool.hatch.metadata.hooks.unidep]\n# Enable the unidep plugin\n\n[tool.hatch.metadata]\nallow-direct-references = true\n\n[tool.unidep]\n# Your dependencies configuration\n```\n\n## :desktop_computer: As a CLI\n\nSee [example](example/) for more information or check the output of `unidep -h` for the available sub commands:\n\n<!-- CODE:BASH:START -->\n<!-- echo '```bash' -->\n<!-- unidep -h -->\n<!-- echo '```' -->\n<!-- CODE:END -->\n<!-- OUTPUT:START -->\n<!-- ⚠️ This content is auto-generated by `markdown-code-runner`. -->\n```bash\nusage: unidep [-h]\n              {merge,install,install-all,conda-lock,pixi,pip-compile,pip,conda,version} ...\n\nUnified Conda and Pip requirements management.\n\npositional arguments:\n  {merge,install,install-all,conda-lock,pixi,pip-compile,pip,conda,version}\n                        Subcommands\n    merge               Combine multiple (or a single) `requirements.yaml` or\n                        `pyproject.toml` files into a single Conda installable\n                        `environment.yaml` file.\n    install             Automatically install all dependencies from one or\n                        more `requirements.yaml` or `pyproject.toml` files.\n                        This command first installs dependencies with Conda,\n                        then with Pip. Finally, it installs local packages\n                        (those containing the `requirements.yaml` or\n                        `pyproject.toml` files) using `pip install [-e]\n                        ./project`.\n    install-all         Install dependencies from all `requirements.yaml` or\n                        `pyproject.toml` files found in the current directory\n                        or specified directory. This command first installs\n                        dependencies using Conda, then Pip, and finally the\n                        local packages.\n    conda-lock          Generate a global `conda-lock.yml` file for a\n                        collection of `requirements.yaml` or `pyproject.toml`\n                        files. Additionally, create individual `conda-\n                        lock.yml` files for each `requirements.yaml` or\n                        `pyproject.toml` file consistent with the global lock\n                        file.\n    pixi                Generate a `pixi.toml` file from `requirements.yaml`\n                        or `pyproject.toml` files.\n    pip-compile         Generate a fully pinned `requirements.txt` file from\n                        one or more `requirements.yaml` or `pyproject.toml`\n                        files using `pip-compile` from `pip-tools`. This\n                        command consolidates all pip dependencies defined in\n                        the `requirements.yaml` or `pyproject.toml` files and\n                        compiles them into a single `requirements.txt` file,\n                        taking into account the specific versions and\n                        dependencies of each package.\n    pip                 Get the pip requirements for the current platform\n                        only.\n    conda               Get the conda requirements for the current platform\n                        only.\n    version             Print version information of unidep.\n\noptions:\n  -h, --help            show this help message and exit\n```\n\n<!-- OUTPUT:END -->\n\n### `unidep merge`\n\nUse `unidep merge` to scan directories for `requirements.yaml` file(s) and combine them into an `environment.yaml` file.\nOptional dependency groups can be included with `--optional-dependencies docs test`\nor `--all-optional-dependencies`.\nSee `unidep merge -h` for more information:\n\n<!-- CODE:BASH:START -->\n<!-- echo '```bash' -->\n<!-- unidep merge -h -->\n<!-- echo '```' -->\n<!-- CODE:END -->\n<!-- OUTPUT:START -->\n<!-- ⚠️ This content is auto-generated by `markdown-code-runner`. -->\n```bash\nusage: unidep merge [-h] [-o OUTPUT] [-n NAME] [--stdout]\n                    [--selector {sel,comment}]\n                    [--optional-dependencies GROUP [GROUP ...] |\n                    --all-optional-dependencies] [-d DIRECTORY]\n                    [--depth DEPTH] [-v]\n                    [-p {linux-64,linux-aarch64,linux-ppc64le,osx-64,osx-arm64,win-64}]\n                    [--skip-dependency SKIP_DEPENDENCY]\n                    [--ignore-pin IGNORE_PIN] [--overwrite-pin OVERWRITE_PIN]\n\nCombine multiple (or a single) `requirements.yaml` or `pyproject.toml` files\ninto a single Conda installable `environment.yaml` file. Example usage:\n`unidep merge --directory . --depth 1 --output environment.yaml` to search for\n`requirements.yaml` or `pyproject.toml` files in the current directory and its\nsubdirectories and create `environment.yaml`. These are the defaults, so you\ncan also just run `unidep merge`. For Pixi support, use `unidep pixi`.\n\noptions:\n  -h, --help            show this help message and exit\n  -o, --output OUTPUT   Output file for the conda environment, by default\n                        `environment.yaml`\n  -n, --name NAME       Name of the conda environment, by default `myenv`\n  --stdout              Output to stdout instead of a file\n  --selector {sel,comment}\n                        The selector to use for the environment markers, if\n                        `sel` then `- numpy # [linux]` becomes `sel(linux):\n                        numpy`, if `comment` then it remains `- numpy #\n                        [linux]`, by default `sel`\n  --optional-dependencies GROUP [GROUP ...]\n                        Include the named optional dependency group(s) from\n                        the discovered requirements files.\n  --all-optional-dependencies\n                        Include all optional dependency groups from the\n                        discovered requirements files.\n  -d, --directory DIRECTORY\n                        Base directory to scan for `requirements.yaml` or\n                        `pyproject.toml` file(s), by default `.`\n  --depth DEPTH         Maximum depth to scan for `requirements.yaml` or\n                        `pyproject.toml` files, by default 1\n  -v, --verbose         Print verbose output\n  -p, --platform {linux-64,linux-aarch64,linux-ppc64le,osx-64,osx-arm64,win-64}\n                        The platform(s) to get the requirements for. Multiple\n                        platforms can be specified. If omitted, behavior is\n                        command-specific: platforms may be inferred from\n                        requirements files, otherwise the current platform is\n                        used.\n  --skip-dependency SKIP_DEPENDENCY\n                        Skip installing a specific dependency that is in one\n                        of the `requirements.yaml` or `pyproject.toml` files.\n                        This option can be used multiple times, each time\n                        specifying a different package to skip. For example,\n                        use `--skip-dependency pandas` to skip installing\n                        pandas.\n  --ignore-pin IGNORE_PIN\n                        Ignore the version pin for a specific package, e.g.,\n                        `--ignore-pin numpy`. This option can be repeated to\n                        ignore multiple packages.\n  --overwrite-pin OVERWRITE_PIN\n                        Overwrite the version pin for a specific package,\n                        e.g., `--overwrite-pin 'numpy=1.19.2'`. This option\n                        can be repeated to overwrite the pins of multiple\n                        packages.\n```\n\n<!-- OUTPUT:END -->\n\n### `unidep install`\n\nUse `unidep install` on one or more `requirements.yaml` files and install the dependencies on the current platform using conda, then install the remaining dependencies with pip, and finally install the current package with `pip install [-e] .`.\nSee `unidep install -h` for more information:\n\n<!-- CODE:BASH:START -->\n<!-- echo '```bash' -->\n<!-- unidep install -h -->\n<!-- echo '```' -->\n<!-- CODE:END -->\n<!-- OUTPUT:START -->\n<!-- ⚠️ This content is auto-generated by `markdown-code-runner`. -->\n```bash\nusage: unidep install [-h] [-v] [-e] [--skip-local] [--skip-pip]\n                      [--skip-conda] [--skip-dependency SKIP_DEPENDENCY]\n                      [--no-dependencies]\n                      [--conda-executable {conda,mamba,micromamba}]\n                      [-n CONDA_ENV_NAME | -p CONDA_ENV_PREFIX] [--dry-run]\n                      [--ignore-pin IGNORE_PIN]\n                      [--overwrite-pin OVERWRITE_PIN] [-f CONDA_LOCK_FILE]\n                      [--no-uv]\n                      files [files ...]\n\nAutomatically install all dependencies from one or more `requirements.yaml` or\n`pyproject.toml` files. This command first installs dependencies with Conda,\nthen with Pip. Finally, it installs local packages (those containing the\n`requirements.yaml` or `pyproject.toml` files) using `pip install [-e]\n./project`. Example usage: `unidep install .` for a single project. For\nmultiple projects: `unidep install ./project1 ./project2`. The command accepts\nboth file paths and directories containing a `requirements.yaml` or\n`pyproject.toml` file. Use `--editable` or `-e` to install the local packages\nin editable mode. See `unidep install-all` to install all `requirements.yaml`\nor `pyproject.toml` files in and below the current folder.\n\npositional arguments:\n  files                 The `requirements.yaml` or `pyproject.toml` file(s) to\n                        parse or folder(s) that contain those file(s), by\n                        default `.`\n\noptions:\n  -h, --help            show this help message and exit\n  -v, --verbose         Print verbose output\n  -e, --editable        Install the project in editable mode\n  --skip-local          Skip installing local dependencies\n  --skip-pip            Skip installing pip dependencies from\n                        `requirements.yaml` or `pyproject.toml`\n  --skip-conda          Skip installing conda dependencies from\n                        `requirements.yaml` or `pyproject.toml`\n  --skip-dependency SKIP_DEPENDENCY\n                        Skip installing a specific dependency that is in one\n                        of the `requirements.yaml` or `pyproject.toml` files.\n                        This option can be used multiple times, each time\n                        specifying a different package to skip. For example,\n                        use `--skip-dependency pandas` to skip installing\n                        pandas.\n  --no-dependencies, --no-deps\n                        Skip installing dependencies from `requirements.yaml`\n                        or `pyproject.toml` file(s) and only install local\n                        package(s). Useful after installing a `conda-lock.yml`\n                        file because then all dependencies have already been\n                        installed.\n  --conda-executable {conda,mamba,micromamba}\n                        The conda executable to use\n  -n, --conda-env-name CONDA_ENV_NAME\n                        Name of the conda environment, if not provided, the\n                        currently active environment name is used, unless\n                        `--conda-env-prefix` is provided\n  -p, --conda-env-prefix CONDA_ENV_PREFIX\n                        Path to the conda environment, if not provided, the\n                        currently active environment path is used, unless\n                        `--conda-env-name` is provided\n  --dry-run, --dry      Only print the commands that would be run\n  --ignore-pin IGNORE_PIN\n                        Ignore the version pin for a specific package, e.g.,\n                        `--ignore-pin numpy`. This option can be repeated to\n                        ignore multiple packages.\n  --overwrite-pin OVERWRITE_PIN\n                        Overwrite the version pin for a specific package,\n                        e.g., `--overwrite-pin 'numpy=1.19.2'`. This option\n                        can be repeated to overwrite the pins of multiple\n                        packages.\n  -f, --conda-lock-file CONDA_LOCK_FILE\n                        Path to the `conda-lock.yml` file to use for creating\n                        the new environment. Assumes that the lock file\n                        contains all dependencies. Must be used with `--conda-\n                        env-name` or `--conda-env-prefix`.\n  --no-uv               Disables the use of `uv` for pip install. By default,\n                        `uv` is used if it is available in the PATH.\n```\n\n<!-- OUTPUT:END -->\n\n### `unidep install-all`\n\nUse `unidep install-all` on a folder with packages that contain `requirements.yaml` files and install the dependencies on the current platform using conda, then install the remaining dependencies with pip, and finally install the current package with `pip install [-e] ./package1 ./package2`.\nSee `unidep install-all -h` for more information:\n\n<!-- CODE:BASH:START -->\n<!-- echo '```bash' -->\n<!-- unidep install -h -->\n<!-- echo '```' -->\n<!-- CODE:END -->\n<!-- OUTPUT:START -->\n<!-- ⚠️ This content is auto-generated by `markdown-code-runner`. -->\n```bash\nusage: unidep install [-h] [-v] [-e] [--skip-local] [--skip-pip]\n                      [--skip-conda] [--skip-dependency SKIP_DEPENDENCY]\n                      [--no-dependencies]\n                      [--conda-executable {conda,mamba,micromamba}]\n                      [-n CONDA_ENV_NAME | -p CONDA_ENV_PREFIX] [--dry-run]\n                      [--ignore-pin IGNORE_PIN]\n                      [--overwrite-pin OVERWRITE_PIN] [-f CONDA_LOCK_FILE]\n                      [--no-uv]\n                      files [files ...]\n\nAutomatically install all dependencies from one or more `requirements.yaml` or\n`pyproject.toml` files. This command first installs dependencies with Conda,\nthen with Pip. Finally, it installs local packages (those containing the\n`requirements.yaml` or `pyproject.toml` files) using `pip install [-e]\n./project`. Example usage: `unidep install .` for a single project. For\nmultiple projects: `unidep install ./project1 ./project2`. The command accepts\nboth file paths and directories containing a `requirements.yaml` or\n`pyproject.toml` file. Use `--editable` or `-e` to install the local packages\nin editable mode. See `unidep install-all` to install all `requirements.yaml`\nor `pyproject.toml` files in and below the current folder.\n\npositional arguments:\n  files                 The `requirements.yaml` or `pyproject.toml` file(s) to\n                        parse or folder(s) that contain those file(s), by\n                        default `.`\n\noptions:\n  -h, --help            show this help message and exit\n  -v, --verbose         Print verbose output\n  -e, --editable        Install the project in editable mode\n  --skip-local          Skip installing local dependencies\n  --skip-pip            Skip installing pip dependencies from\n                        `requirements.yaml` or `pyproject.toml`\n  --skip-conda          Skip installing conda dependencies from\n                        `requirements.yaml` or `pyproject.toml`\n  --skip-dependency SKIP_DEPENDENCY\n                        Skip installing a specific dependency that is in one\n                        of the `requirements.yaml` or `pyproject.toml` files.\n                        This option can be used multiple times, each time\n                        specifying a different package to skip. For example,\n                        use `--skip-dependency pandas` to skip installing\n                        pandas.\n  --no-dependencies, --no-deps\n                        Skip installing dependencies from `requirements.yaml`\n                        or `pyproject.toml` file(s) and only install local\n                        package(s). Useful after installing a `conda-lock.yml`\n                        file because then all dependencies have already been\n                        installed.\n  --conda-executable {conda,mamba,micromamba}\n                        The conda executable to use\n  -n, --conda-env-name CONDA_ENV_NAME\n                        Name of the conda environment, if not provided, the\n                        currently active environment name is used, unless\n                        `--conda-env-prefix` is provided\n  -p, --conda-env-prefix CONDA_ENV_PREFIX\n                        Path to the conda environment, if not provided, the\n                        currently active environment path is used, unless\n                        `--conda-env-name` is provided\n  --dry-run, --dry      Only print the commands that would be run\n  --ignore-pin IGNORE_PIN\n                        Ignore the version pin for a specific package, e.g.,\n                        `--ignore-pin numpy`. This option can be repeated to\n                        ignore multiple packages.\n  --overwrite-pin OVERWRITE_PIN\n                        Overwrite the version pin for a specific package,\n                        e.g., `--overwrite-pin 'numpy=1.19.2'`. This option\n                        can be repeated to overwrite the pins of multiple\n                        packages.\n  -f, --conda-lock-file CONDA_LOCK_FILE\n                        Path to the `conda-lock.yml` file to use for creating\n                        the new environment. Assumes that the lock file\n                        contains all dependencies. Must be used with `--conda-\n                        env-name` or `--conda-env-prefix`.\n  --no-uv               Disables the use of `uv` for pip install. By default,\n                        `uv` is used if it is available in the PATH.\n```\n\n<!-- OUTPUT:END -->\n\n### `unidep conda-lock`\n\nUse `unidep conda-lock` on one or multiple `requirements.yaml` files and output the conda-lock file.\nOptionally, when using a monorepo with multiple subpackages (with their own `requirements.yaml` files), generate a lock file for each subpackage.\nSee `unidep conda-lock -h` for more information:\n\n<!-- CODE:BASH:START -->\n<!-- echo '```bash' -->\n<!-- unidep conda-lock -h -->\n<!-- echo '```' -->\n<!-- CODE:END -->\n<!-- OUTPUT:START -->\n<!-- ⚠️ This content is auto-generated by `markdown-code-runner`. -->\n```bash\nusage: unidep conda-lock [-h] [--only-global] [--lockfile LOCKFILE]\n                         [--check-input-hash] [-d DIRECTORY] [--depth DEPTH]\n                         [-f FILE] [-v]\n                         [-p {linux-64,linux-aarch64,linux-ppc64le,osx-64,osx-arm64,win-64}]\n                         [--skip-dependency SKIP_DEPENDENCY]\n                         [--ignore-pin IGNORE_PIN]\n                         [--overwrite-pin OVERWRITE_PIN]\n                         ...\n\nGenerate a global `conda-lock.yml` file for a collection of\n`requirements.yaml` or `pyproject.toml` files. Additionally, create individual\n`conda-lock.yml` files for each `requirements.yaml` or `pyproject.toml` file\nconsistent with the global lock file. Example usage: `unidep conda-lock\n--directory ./projects` to generate conda-lock files for all\n`requirements.yaml` or `pyproject.toml` files in the `./projects` directory.\nUse `--only-global` to generate only the global lock file. The `--check-input-\nhash` option can be used to avoid regenerating lock files if the input hasn't\nchanged.\n\npositional arguments:\n  extra_flags           Extra flags to pass to `conda-lock lock`. These flags\n                        are passed directly and should be provided in the\n                        format expected by `conda-lock lock`. For example,\n                        `unidep conda-lock -- --micromamba`. Note that the\n                        `--` is required to separate the flags for `unidep\n                        conda-lock` from the flags for `conda-lock lock`.\n\noptions:\n  -h, --help            show this help message and exit\n  --only-global         Only generate the global lock file\n  --lockfile LOCKFILE   Specify a path for the global lockfile (default:\n                        `conda-lock.yml` in current directory). Path should be\n                        relative, e.g., `--lockfile ./locks/example.conda-\n                        lock.yml`.\n  --check-input-hash    Check existing input hashes in lockfiles before\n                        regenerating lock files. This flag is directly passed\n                        to `conda-lock`.\n  -d, --directory DIRECTORY\n                        Base directory to scan for `requirements.yaml` or\n                        `pyproject.toml` file(s), by default `.`\n  --depth DEPTH         Maximum depth to scan for `requirements.yaml` or\n                        `pyproject.toml` files, by default 1\n  -f, --file FILE       A single `requirements.yaml` or `pyproject.toml` file\n                        to use, or folder that contains that file. This is an\n                        alternative to using `--directory` which searches for\n                        all `requirements.yaml` or `pyproject.toml` files in\n                        the directory and its subdirectories.\n  -v, --verbose         Print verbose output\n  -p, --platform {linux-64,linux-aarch64,linux-ppc64le,osx-64,osx-arm64,win-64}\n                        The platform(s) to get the requirements for. Multiple\n                        platforms can be specified. If omitted, behavior is\n                        command-specific: platforms may be inferred from\n                        requirements files, otherwise the current platform is\n                        used.\n  --skip-dependency SKIP_DEPENDENCY\n                        Skip installing a specific dependency that is in one\n                        of the `requirements.yaml` or `pyproject.toml` files.\n                        This option can be used multiple times, each time\n                        specifying a different package to skip. For example,\n                        use `--skip-dependency pandas` to skip installing\n                        pandas.\n  --ignore-pin IGNORE_PIN\n                        Ignore the version pin for a specific package, e.g.,\n                        `--ignore-pin numpy`. This option can be repeated to\n                        ignore multiple packages.\n  --overwrite-pin OVERWRITE_PIN\n                        Overwrite the version pin for a specific package,\n                        e.g., `--overwrite-pin 'numpy=1.19.2'`. This option\n                        can be repeated to overwrite the pins of multiple\n                        packages.\n```\n\n<!-- OUTPUT:END -->\n\n### `unidep pixi`\n\nUse `unidep pixi` to generate a `pixi.toml` file from your `requirements.yaml` or `pyproject.toml` files.\nThis enables using [Pixi](https://pixi.sh/) for solving/locking/installing while keeping UniDep as your source of truth.\n\nThe philosophy is **\"Let UniDep translate, let Pixi resolve\"**.\n\n**Workflow:**\n```bash\n# 1. Generate pixi.toml from your requirements\nunidep pixi\n\n# 2. Use pixi directly\npixi install\npixi lock\npixi run <cmd>\n```\n\n#### What `unidep pixi` generates\n\n- A `[workspace]` section with `name`, `channels`, and `platforms`\n- Conda deps in `[dependencies]`\n- PyPI deps in `[pypi-dependencies]`\n- Selector/platform-specific deps in `[target.<platform>.dependencies]` and/or `[target.<platform>.pypi-dependencies]`\n- Optional dependency groups as Pixi features (`[feature.<group>.*]`)\n- Local installable projects as editable path deps:\n  ```toml\n  [pypi-dependencies]\n  my_pkg = { path = \"./relative/path\", editable = true }\n  ```\n\nIn monorepo mode (multiple input files), UniDep builds feature sections per discovered project and composes environments from those features.\n\n#### Dependency reconciliation rules (important)\n\nWhen the same package appears from both conda and pip, UniDep applies deterministic rules before writing `pixi.toml`:\n\n1. If pip has extras (`foo[bar]`), pip wins.\n2. If only one side is pinned, pinned wins.\n3. On ties (both pinned or both unpinned), conda wins.\n4. When both sides are pinned and one declaration is narrower in platform scope, the narrower target-specific intent wins on that target. Other platforms continue through the same shared selection rules independently.\n\nVersion pins from repeated entries are merged when possible (for example `>=1.7,<2` + `<1.16` → `>=1.7,<1.16`).\n\n#### Channels/platforms precedence\n\n- **Channels**\n  - If `--channel` is passed: use only CLI-provided channels.\n  - Else: collect channels from requirement files.\n  - Else fallback: `conda-forge`.\n- **Platforms**\n  - If `--platform` is passed: use CLI-provided platforms.\n  - Else: use platforms declared in files.\n  - Else: infer from selectors in dependencies.\n  - Else fallback: current platform.\n\n#### Example (single-file)\n\nInput (`requirements.yaml`):\n\n```yaml\nchannels:\n  - conda-forge\ndependencies:\n  - numpy >=1.26\n  - pip: rich\n  - pip: uvloop  # [linux64]\noptional_dependencies:\n  dev:\n    - pytest\nplatforms:\n  - linux-64\n  - osx-64\n```\n\nRepresentative output shape (`pixi.toml`):\n\n```toml\n[workspace]\nname = \"my-project\"\nchannels = [\"conda-forge\"]\nplatforms = [\"linux-64\", \"osx-64\"]\n\n[dependencies]\nnumpy = \">=1.26\"\n\n[pypi-dependencies]\nrich = \"*\"\n\n[target.linux-64.pypi-dependencies]\nuvloop = \"*\"\n\n[feature.dev.dependencies]\npytest = \"*\"\n\n[environments]\ndefault = []\ndev = [\"dev\"]\n```\n\nSee `unidep pixi -h` for more information:\n\n<!-- CODE:BASH:START -->\n<!-- echo '```bash' -->\n<!-- unidep pixi -h -->\n<!-- echo '```' -->\n<!-- CODE:END -->\n<!-- OUTPUT:START -->\n<!-- ⚠️ This content is auto-generated by `markdown-code-runner`. -->\n```bash\nusage: unidep pixi [-h] [-o OUTPUT] [-n NAME] [--stdout] [-c CHANNEL]\n                   [-d DIRECTORY] [--depth DEPTH] [-f FILE] [-v]\n                   [-p {linux-64,linux-aarch64,linux-ppc64le,osx-64,osx-arm64,win-64}]\n                   [--skip-dependency SKIP_DEPENDENCY]\n                   [--ignore-pin IGNORE_PIN] [--overwrite-pin OVERWRITE_PIN]\n\nGenerate a `pixi.toml` file from `requirements.yaml` or `pyproject.toml`\nfiles. Example usage: `unidep pixi` to generate a pixi.toml file. Use\n`--output` to specify a different output path. Use `--name` to set the project\nname. After generating, use `pixi lock` and `pixi install` directly.\n\noptions:\n  -h, --help            show this help message and exit\n  -o, --output OUTPUT   Output path for pixi.toml (default: pixi.toml in\n                        current directory)\n  -n, --name NAME       Name of the project (default: current directory name)\n  --stdout              Output to stdout instead of a file\n  -c, --channel CHANNEL\n                        Conda channel to include. Can be repeated. Overrides\n                        channels declared in requirements files. If omitted,\n                        channels are read from the requirements files\n                        (defaulting to conda-forge).\n  -d, --directory DIRECTORY\n                        Base directory to scan for `requirements.yaml` or\n                        `pyproject.toml` file(s), by default `.`\n  --depth DEPTH         Maximum depth to scan for `requirements.yaml` or\n                        `pyproject.toml` files, by default 1\n  -f, --file FILE       A single `requirements.yaml` or `pyproject.toml` file\n                        to use, or folder that contains that file. This is an\n                        alternative to using `--directory` which searches for\n                        all `requirements.yaml` or `pyproject.toml` files in\n                        the directory and its subdirectories.\n  -v, --verbose         Print verbose output\n  -p, --platform {linux-64,linux-aarch64,linux-ppc64le,osx-64,osx-arm64,win-64}\n                        The platform(s) to get the requirements for. Multiple\n                        platforms can be specified. If omitted, behavior is\n                        command-specific: platforms may be inferred from\n                        requirements files, otherwise the current platform is\n                        used.\n  --skip-dependency SKIP_DEPENDENCY\n                        Skip installing a specific dependency that is in one\n                        of the `requirements.yaml` or `pyproject.toml` files.\n                        This option can be used multiple times, each time\n                        specifying a different package to skip. For example,\n                        use `--skip-dependency pandas` to skip installing\n                        pandas.\n  --ignore-pin IGNORE_PIN\n                        Ignore the version pin for a specific package, e.g.,\n                        `--ignore-pin numpy`. This option can be repeated to\n                        ignore multiple packages.\n  --overwrite-pin OVERWRITE_PIN\n                        Overwrite the version pin for a specific package,\n                        e.g., `--overwrite-pin 'numpy=1.19.2'`. This option\n                        can be repeated to overwrite the pins of multiple\n                        packages.\n```\n\n<!-- OUTPUT:END -->\n\n> [!TIP]\n> Install Pixi-related optional dependencies with: `pip install \"unidep[pixi]\"`\n\n### `unidep pip-compile`\n\nUse `unidep pip-compile` on one or multiple `requirements.yaml` files and output a fully locked `requirements.txt` file using `pip-compile` from [`pip-tools`](https://pip-tools.readthedocs.io/en/latest/).\nSee `unidep pip-compile -h` for more information:\n\n<!-- CODE:BASH:START -->\n<!-- echo '```bash' -->\n<!-- unidep pip-compile -h -->\n<!-- echo '```' -->\n<!-- CODE:END -->\n<!-- OUTPUT:START -->\n<!-- ⚠️ This content is auto-generated by `markdown-code-runner`. -->\n```bash\nusage: unidep pip-compile [-h] [-o OUTPUT_FILE] [-d DIRECTORY] [--depth DEPTH]\n                          [-v]\n                          [-p {linux-64,linux-aarch64,linux-ppc64le,osx-64,osx-arm64,win-64}]\n                          [--skip-dependency SKIP_DEPENDENCY]\n                          [--ignore-pin IGNORE_PIN]\n                          [--overwrite-pin OVERWRITE_PIN]\n                          ...\n\nGenerate a fully pinned `requirements.txt` file from one or more\n`requirements.yaml` or `pyproject.toml` files using `pip-compile` from `pip-\ntools`. This command consolidates all pip dependencies defined in the\n`requirements.yaml` or `pyproject.toml` files and compiles them into a single\n`requirements.txt` file, taking into account the specific versions and\ndependencies of each package. Example usage: `unidep pip-compile --directory\n./projects` to generate a `requirements.txt` file for all `requirements.yaml`\nor `pyproject.toml` files in the `./projects` directory. Use `--output-file\nrequirements.txt` to specify a different output file.\n\npositional arguments:\n  extra_flags           Extra flags to pass to `pip-compile`. These flags are\n                        passed directly and should be provided in the format\n                        expected by `pip-compile`. For example, `unidep pip-\n                        compile -- --generate-hashes --allow-unsafe`. Note\n                        that the `--` is required to separate the flags for\n                        `unidep pip-compile` from the flags for `pip-compile`.\n\noptions:\n  -h, --help            show this help message and exit\n  -o, --output-file OUTPUT_FILE\n                        Output file for the pip requirements, by default\n                        `requirements.txt`\n  -d, --directory DIRECTORY\n                        Base directory to scan for `requirements.yaml` or\n                        `pyproject.toml` file(s), by default `.`\n  --depth DEPTH         Maximum depth to scan for `requirements.yaml` or\n                        `pyproject.toml` files, by default 1\n  -v, --verbose         Print verbose output\n  -p, --platform {linux-64,linux-aarch64,linux-ppc64le,osx-64,osx-arm64,win-64}\n                        The platform(s) to get the requirements for. Multiple\n                        platforms can be specified. If omitted, behavior is\n                        command-specific: platforms may be inferred from\n                        requirements files, otherwise the current platform is\n                        used.\n  --skip-dependency SKIP_DEPENDENCY\n                        Skip installing a specific dependency that is in one\n                        of the `requirements.yaml` or `pyproject.toml` files.\n                        This option can be used multiple times, each time\n                        specifying a different package to skip. For example,\n                        use `--skip-dependency pandas` to skip installing\n                        pandas.\n  --ignore-pin IGNORE_PIN\n                        Ignore the version pin for a specific package, e.g.,\n                        `--ignore-pin numpy`. This option can be repeated to\n                        ignore multiple packages.\n  --overwrite-pin OVERWRITE_PIN\n                        Overwrite the version pin for a specific package,\n                        e.g., `--overwrite-pin 'numpy=1.19.2'`. This option\n                        can be repeated to overwrite the pins of multiple\n                        packages.\n```\n\n<!-- OUTPUT:END -->\n\n### `unidep pip`\n\nUse `unidep pip` on a `requirements.yaml` file and output the pip installable dependencies on the current platform (default).\nSee `unidep pip -h` for more information:\n\n<!-- CODE:BASH:START -->\n<!-- echo '```bash' -->\n<!-- unidep pip -h -->\n<!-- echo '```' -->\n<!-- CODE:END -->\n<!-- OUTPUT:START -->\n<!-- ⚠️ This content is auto-generated by `markdown-code-runner`. -->\n```bash\nusage: unidep pip [-h] [-f FILE] [-v]\n                  [-p {linux-64,linux-aarch64,linux-ppc64le,osx-64,osx-arm64,win-64}]\n                  [--skip-dependency SKIP_DEPENDENCY]\n                  [--ignore-pin IGNORE_PIN] [--overwrite-pin OVERWRITE_PIN]\n                  [--separator SEPARATOR]\n\nGet the pip requirements for the current platform only. Example usage: `unidep\npip --file folder1 --file folder2/requirements.yaml --separator ' ' --platform\nlinux-64` to extract all the pip dependencies specific to the linux-64\nplatform. Note that the `--file` argument can be used multiple times to\nspecify multiple `requirements.yaml` or `pyproject.toml` files and that --file\ncan also be a folder that contains a `requirements.yaml` or `pyproject.toml`\nfile.\n\noptions:\n  -h, --help            show this help message and exit\n  -f, --file FILE       The `requirements.yaml` or `pyproject.toml` file to\n                        parse, or folder that contains that file, by default\n                        `.`\n  -v, --verbose         Print verbose output\n  -p, --platform {linux-64,linux-aarch64,linux-ppc64le,osx-64,osx-arm64,win-64}\n                        The platform(s) to get the requirements for. Multiple\n                        platforms can be specified. If omitted, behavior is\n                        command-specific: platforms may be inferred from\n                        requirements files, otherwise the current platform is\n                        used.\n  --skip-dependency SKIP_DEPENDENCY\n                        Skip installing a specific dependency that is in one\n                        of the `requirements.yaml` or `pyproject.toml` files.\n                        This option can be used multiple times, each time\n                        specifying a different package to skip. For example,\n                        use `--skip-dependency pandas` to skip installing\n                        pandas.\n  --ignore-pin IGNORE_PIN\n                        Ignore the version pin for a specific package, e.g.,\n                        `--ignore-pin numpy`. This option can be repeated to\n                        ignore multiple packages.\n  --overwrite-pin OVERWRITE_PIN\n                        Overwrite the version pin for a specific package,\n                        e.g., `--overwrite-pin 'numpy=1.19.2'`. This option\n                        can be repeated to overwrite the pins of multiple\n                        packages.\n  --separator SEPARATOR\n                        The separator between the dependencies, by default ` `\n```\n\n<!-- OUTPUT:END -->\n\n### `unidep conda`\n\nUse `unidep conda` on a `requirements.yaml` file and output the conda installable dependencies on the current platform (default).\nSee `unidep conda -h` for more information:\n\n<!-- CODE:BASH:START -->\n<!-- echo '```bash' -->\n<!-- unidep conda -h -->\n<!-- echo '```' -->\n<!-- CODE:END -->\n<!-- OUTPUT:START -->\n<!-- ⚠️ This content is auto-generated by `markdown-code-runner`. -->\n```bash\nusage: unidep conda [-h] [-f FILE] [-v]\n                    [-p {linux-64,linux-aarch64,linux-ppc64le,osx-64,osx-arm64,win-64}]\n                    [--skip-dependency SKIP_DEPENDENCY]\n                    [--ignore-pin IGNORE_PIN] [--overwrite-pin OVERWRITE_PIN]\n                    [--separator SEPARATOR]\n\nGet the conda requirements for the current platform only. Example usage:\n`unidep conda --file folder1 --file folder2/requirements.yaml --separator ' '\n--platform linux-64` to extract all the conda dependencies specific to the\nlinux-64 platform. Note that the `--file` argument can be used multiple times\nto specify multiple `requirements.yaml` or `pyproject.toml` files and that\n--file can also be a folder that contains a `requirements.yaml` or\n`pyproject.toml` file.\n\noptions:\n  -h, --help            show this help message and exit\n  -f, --file FILE       The `requirements.yaml` or `pyproject.toml` file to\n                        parse, or folder that contains that file, by default\n                        `.`\n  -v, --verbose         Print verbose output\n  -p, --platform {linux-64,linux-aarch64,linux-ppc64le,osx-64,osx-arm64,win-64}\n                        The platform(s) to get the requirements for. Multiple\n                        platforms can be specified. If omitted, behavior is\n                        command-specific: platforms may be inferred from\n                        requirements files, otherwise the current platform is\n                        used.\n  --skip-dependency SKIP_DEPENDENCY\n                        Skip installing a specific dependency that is in one\n                        of the `requirements.yaml` or `pyproject.toml` files.\n                        This option can be used multiple times, each time\n                        specifying a different package to skip. For example,\n                        use `--skip-dependency pandas` to skip installing\n                        pandas.\n  --ignore-pin IGNORE_PIN\n                        Ignore the version pin for a specific package, e.g.,\n                        `--ignore-pin numpy`. This option can be repeated to\n                        ignore multiple packages.\n  --overwrite-pin OVERWRITE_PIN\n                        Overwrite the version pin for a specific package,\n                        e.g., `--overwrite-pin 'numpy=1.19.2'`. This option\n                        can be repeated to overwrite the pins of multiple\n                        packages.\n  --separator SEPARATOR\n                        The separator between the dependencies, by default ` `\n```\n\n<!-- OUTPUT:END -->\n\n## ❓ FAQ\n\nHere is a list of questions we have either been asked by users or potential pitfalls we hope to help users avoid:\n\n### **Q: When to use UniDep?**\n\n**A:** UniDep is particularly useful for setting up full development environments that require both Python *and* non-Python dependencies (e.g., CUDA, compilers, etc.) with a single command.\n\nIn fields like research, data science, robotics, AI, and ML projects, it is common to work from a locally cloned Git repository.\n\nSetting up a full development environment can be a pain, especially if you need to install non Python dependencies like compilers, low-level numerical libraries, or CUDA (luckily Conda has all of them).\nTypically, instructions are different for each OS and their corresponding package managers (`apt`, `brew`, `yum`, `winget`, etc.).\n\nWith UniDep, you can specify all your Pip and Conda dependencies in a single file.\nTo get set up on a new machine, you just need to install Conda (we recommend [micromamba](https://mamba.readthedocs.io/en/latest/installation/micromamba-installation.html)) and run `pip install unidep; unidep install-all -e` in your project directory, to install all dependencies and local packages in editable mode in the current Conda environment.\n\nFor fully reproducible environments, you can run `unidep conda-lock` to generate a `conda-lock.yml` file.\nThen, run `conda env create -f conda-lock.yml -n myenv` to create a new Conda environment with all the third-party dependencies.\nFinally, run `unidep install-all -e --no-dependencies` to install all your local packages in editable mode.\n\nFor those who prefer not to use Conda, you can simply run `pip install -e .` on a project using UniDep.\nYou'll need to install the non-Python dependencies yourself, but you'll have a list of them in the `requirements.yaml` file.\n\nIn summary, use UniDep if you:\n\n- Prefer installing packages with conda but still want your package to be pip installable.\n- Are tired of synchronizing your Pip requirements (`requirements.txt`) and Conda requirements (`environment.yaml`).\n- Want a low-effort, comprehensive development environment setup.\n\n### **Q: Just show me a full example!**\n\n**A:** Check out the [`example` folder](https://github.com/basnijholt/unidep/tree/main/example).\n\n### **Q: Uses of UniDep in the wild?**\n\n**A:** UniDep really shines when used in a monorepo with multiple dependent projects, however, since these are typically private, we cannot share them.\n\nHowever, an example of a single package that is public is [`home-assistant-streamdeck-yaml`](https://github.com/basnijholt/home-assistant-streamdeck-yaml/).\nThis is a Python package that allows to interact with [Home Assistant](https://www.home-assistant.io/) from an Elgato Stream Deck connected via USB to e.g., a Raspberry Pi.\nIt requires a couple of system dependencies (e.g., `libusb` and `hidapi`), which are typically installed with `apt` or `brew`.\nThe [`README.md`](https://github.com/basnijholt/home-assistant-streamdeck-yaml/blob/main/README.md) shows different installation instructions on Linux, MacOS, and Windows for non-Conda installs, however, with UniDep, we can just use `unidep install .` on all platforms.\nIt is fully configured via [`pyproject.toml`](https://github.com/basnijholt/home-assistant-streamdeck-yaml/blob/main/pyproject.toml).\nThe 2 `Dockerfile`s show 2 different ways of using UniDep:\n\n1. [`Dockerfile.locked`](https://github.com/basnijholt/home-assistant-streamdeck-yaml/blob/a1b9966398dfe748804f058f82d546e47cd7f722/Dockerfile.locked): Installing `conda-lock.yml` (generated with `unidep conda-lock`) and then `pip install .` the local package.\n2. [`Dockerfile.latest`](https://github.com/basnijholt/home-assistant-streamdeck-yaml/blob/a1b9966398dfe748804f058f82d546e47cd7f722/Dockerfile.latest): Using `unidep install .` to install all dependencies, first with conda, then pip, then the local package.\n\n### **Q: How do I force PyPI instead of a local path for one dependency?**\n\n**A:** Use `use: pypi` to force the PyPI package even during development (see [Overriding Nested Vendor Copies](#overriding-nested-vendor-copies-with-use)). This is especially useful for overriding nested vendor copies while keeping other local dependencies editable.\n\n```yaml\nlocal_dependencies:\n  - local: ./path/to/dep\n    pypi: my-package>=1.0\n    use: pypi  # Force PyPI, skip local path\n```\n\n### **Q: How do I ignore a local dependency entirely?**\n\n**A:** Set `use: skip` on that entry. It won't be installed and UniDep won't recurse into it. See [Overriding Nested Vendor Copies](#overriding-nested-vendor-copies-with-use) for details.\n\n### **Q: A submodule brings its own copy of package X. How do I avoid conflicts?**\n\n**A:** Use `use: pypi` as shown in [Overriding Nested Vendor Copies](#overriding-nested-vendor-copies-with-use). In short:\n\n```yaml\nlocal_dependencies:\n  - ./third_party/foo              # Keep foo editable\n  - local: ./third_party/foo/third_party/bar\n    pypi: my-bar>=2.0\n    use: pypi                      # Force YOUR PyPI build of bar\n```\n\nThis propagates to **every** nested reference, so foo's bundled bar gets replaced with your PyPI package.\n\n### **Q: How is this different from conda/mamba/pip?**\n\n**A:** UniDep uses pip and conda under the hood to install dependencies, but it is not a replacement for them. UniDep will print the commands it runs, so you can see exactly what it is doing.\n\n### **Q: I found a project using unidep, now what?**\n\n**A:** You can install it like *any other Python package* using `pip install`.\nHowever, to take full advantage of UniDep's functionality, clone the repository and run `unidep install-all -e` in the project directory.\nThis installs all dependencies in editable mode in the current Conda environment.\n\n### **Q: How to handle local dependencies that do not use UniDep?**\n\n**A:** You can use the `local_dependencies` field in the `requirements.yaml` or `pyproject.toml` file to specify local dependencies.\nHowever, *if* a local dependency is *not* managed by UniDep, it will skip installing its dependencies!\n\nTo include all its dependencies, either convert the package to use UniDep (🏆), or maintain a separate `requirements.yaml` file, e.g., for a package called `foo` create, `foo-requirements.yaml`:\n\n```yaml\ndependencies:\n  # List the dependencies of foo here\n  - numpy\n  - scipy\n  - matplotlib\n  - bar\nlocal_dependencies:\n  - ./path/to/foo  # This is the path to the package\n```\n\nThen, in the `requirements.yaml` or `pyproject.toml` file of the package that uses `foo`, list `foo-requirements.yaml` as a local dependency:\n\n```yaml\nlocal_dependencies:\n  - ./path/to/foo-requirements.yaml\n```\n\n### **Q: Can't Conda already do this?**\n\n**A:** Not quite. Conda can indeed install both Conda and Pip dependencies via an `environment.yaml` file, however, it does not work the other way around.\nPip cannot install the `pip` dependencies from an `environment.yaml` file.\nThis means, that if you want your package to be installable with `pip install -e .` *and* support Conda, you need to maintain two separate files: `environment.yaml` and `requirements.txt` (or specify these dependencies in `pyproject.toml` or `setup.py`).\n\n### **Q: What is the difference between `conda-lock` and `unidep conda-lock`?**\n\n**A:** [`conda-lock`](https://github.com/conda/conda-lock) is a standalone tool that creates a `conda-lock.yml` file from a `environment.yaml` file.\nOn the other hand, `unidep conda-lock` is a command within the UniDep tool that also generates a `conda-lock.yml` file (leveraging `conda-lock`), but it does so from one or more `requirements.yaml` or `pyproject.toml` files.\nWhen managing multiple dependent projects (e.g., in a monorepo), a unique feature of `unidep conda-lock` is its ability to create **_consistent_** individual `conda-lock.yml` files for each `requirements.yaml` or `pyproject.toml` file, ensuring consistency with a global `conda-lock.yml` file.\nThis feature is not available in the standalone `conda-lock` tool.\n\n### **Q: What is the difference between `hatch-conda` / `pdm-conda` and `unidep`?**\n\n**A:** [`hatch-conda`](https://github.com/OldGrumpyViking/hatch-conda) is a plugin for [`hatch`](https://hatch.pypa.io/latest/) that integrates Conda environments into `hatch`.\nA key difference is that `hatch-conda` keeps Conda and Pip dependencies separate, choosing to install packages with either Conda *or* Pip.\nThis results in Conda being a hard requirement, for example, if `numba` is specified for Conda, it cannot be installed with Pip despite its availability on PyPI.\n\nIn contrast, [UniDep](https://github.com/basnijholt/unidep/) does not require Conda.\nWithout Conda, it can still install any dependency that is available on PyPI (e.g., `numba` is both Conda and Pip installable).\nHowever, without Conda, UniDep will not install dependencies exclusive to Conda.\nThese Conda-specific dependencies can often be installed through alternative package managers like `apt`, `brew`, `yum`, or by building them from source.\n\nAnother key difference is that `hatch-conda` is managing [Hatch environments](https://hatch.pypa.io/latest/environment/) whereas `unidep` can install Pip dependencies in the current Python environment (venv, Conda, Hatch, etc.), however, to optimally use UniDep, we recommend using Conda environments to additionally install non-Python dependencies.\n\nSimilar to `hatch-conda`, `unidep` also integrates with Hatchling, but it works in a slightly different way.\n\n**A:** [`pdm-conda`](https://github.com/macro128/pdm-conda) is a plugin for [`pdm`](https://pdm-project.org/) designed to facilitate the use of Conda environments in conjunction with `pdm`.\nLike `hatch-conda`, `pdm-conda` opts to install packages either with Conda or Pip.\nIt is closely integrated with `pdm`, primarily enabling the inclusion of Conda packages in `pdm`'s lock file (`pdm.lock`).\nHowever, `pdm-conda` lacks extensive cross-platform support.\nFor instance, when adding a package like Numba using `pdm-conda`, it gets locked to the current platform (e.g., osx-arm64) without the flexibility to specify compatibility for other platforms such as linux64.\nIn contrast, UniDep allows for cross-platform compatibility, enabling the user to specify dependencies for multiple platforms.\nUniDep currently does not support `pdm`, but it does support Hatchling and Setuptools.\n\nUniDep stands out from both `pdm-conda` and `hatch-conda` with its additional functionalities, particularly beneficial for monorepos and projects spanning multiple operating systems. For instance:\n\n1. **Conda Lock Files**: Create `conda-lock.yml` files for all packages with consistent sub-lock files per package.\n2. **CLI tools**: Provides tools like `unidep install-all -e` which will install multiple local projects (e.g., in monorepo) and all its dependencies first with Conda, then remaining ones with Pip, and finally the local dependencies in editable mode with Pip.\n3. **Conda Environment Files**: Can create standard Conda `environment.yaml` files by combining the dependencies from many `requirements.yaml` or `pyproject.toml` files.\n4. **Platform-Specific Dependencies**: Allows specifying dependencies for certain platforms (e.g., linux64, osx-arm64), enhancing cross-platform compatibility.\n\n## :hammer_and_wrench: Troubleshooting\n\n### `pip install` fails with `FileNotFoundError`\n\nWhen using a project that uses `local_dependencies: [../not/current/dir]` in the `requirements.yaml` file:\n\n```yaml\nlocal_dependencies:\n  # File in a different directory than the pyproject.toml file\n  - ../common-requirements.yaml\n```\n\nYou might get an error like this when using a `pip` version older than `22.0`:\n\n```bash\n$ pip install /path/to/your/project/using/unidep\n  ...\n  File \"/usr/lib/python3.8/pathlib.py\", line 1222, in open\n    return io.open(self, mode, buffering, encoding, errors, newline,\n  File \"/usr/lib/python3.8/pathlib.py\", line 1078, in _opener\n    return self._accessor.open(self, flags, mode)\nFileNotFoundError: [Errno 2] No such file or directory: '/tmp/common-requirements.yaml'\n```\n\nThe solution is to upgrade `pip` to version `22.0` or newer:\n\n```bash\npip install --upgrade pip\n```\n\n## :warning: Limitations\n\n- **Conda-Focused**: Best suited for Conda environments. However, note that having `conda` is not a requirement to install packages that use UniDep.\n- **Setuptools and Hatchling only**: Currently only works with setuptools and Hatchling, not flit, poetry, or other build systems. Open an issue if you'd like to see support for other build systems.\n- No [logic operators in platform selectors](https://github.com/basnijholt/unidep/issues/5) and [no Python selectors](https://github.com/basnijholt/unidep/issues/7).\n\n* * *\n\nTry `unidep` today for a streamlined approach to managing your Conda environment dependencies across multiple projects! 🎉👏\n"
  },
  {
    "path": "bootstrap.sh",
    "content": "#!/usr/bin/env bash\n# Run this script with:\n#   \"${SHELL}\" <(curl -LsSf raw.githubusercontent.com/basnijholt/unidep/main/bootstrap.sh)\n#\n# 🚀 UniDep - Unified Conda and Pip Dependency Management 🚀\n#\n# This script downloads and installs:\n#  - micromamba to ~/.local/bin/micromamba (for fast Conda environment management)\n#  - uv to ~/.local/bin/uv (for fast pip installations)\n#  - unidep (to manage unified Conda and Pip dependencies)\n#\n# UniDep streamlines Python project dependency management by combining both Conda\n# and Pip dependencies into a single system. For more information, visit:\n# https://github.com/basnijholt/unidep\n#\n# If you prefer to run the commands manually, you can execute each section one by one.\n# Otherwise, piping this script directly to your default shell ensures everything is installed in one go.\n\necho \"Downloading and installing micromamba to ~/.local/bin/micromamba and uv to ~/.local/bin/uv\"\n\n# Install micromamba (https://mamba.readthedocs.io/en/latest/installation/micromamba-installation.html)\n\"${SHELL}\" <(curl -LsSf micro.mamba.pm/install.sh) < /dev/null\n\n# Install uv (https://docs.astral.sh/uv/getting-started/installation/)\ncurl -LsSf https://astral.sh/uv/install.sh | sh\n\n# Install unidep using uv\n~/.local/bin/uv tool install --quiet -U \"unidep[all]\"\n\necho \"Done installing micromamba, uv, and unidep\"\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = source\nBUILDDIR      = build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\nclean:\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\trm -rf $(BUILDDIR)/*\n\trm -f $(SOURCEDIR)/*.md\n"
  },
  {
    "path": "docs/source/.gitignore",
    "content": "*.md\n"
  },
  {
    "path": "docs/source/conf.py",
    "content": "\"\"\"Spinx configuration file for the unidep documentation.\n\nThe documentation is generated from the README.md file in the root of the repository.\nThe README.md file is copied to the Sphinx source directory and processed to generate\nthe documentation. The following transformations are applied to the README.md file:\n\n1. Replace named emojis with unicode emojis.\n2. Replace markdown alerts with admonitions.\n3. Replace relative links to `example/` files with absolute links to GitHub.\n4. Fix anchors with named emojis.\n5. Split the README.md file into individual sections based on second-level headers.\n6. Extract the table of contents links from the processed README.\n7. Replace links in each section to point to the correct section.\n8. Decrease the header levels by one in each section.\n9. Rename the first section to `introduction.md` and update its header.\n10. Write an index file for the documentation.\n\nThis code is tightly coupled with the structure of the README.md file and the\ntable of contents generated by the doctoc tool.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport re\nimport shutil\nimport sys\nimport textwrap\nfrom pathlib import Path\n\npackage_path = Path(\"../..\").resolve()\nsys.path.insert(0, str(package_path))\nPYTHON_PATH = os.environ.get(\"PYTHONPATH\", \"\")\nos.environ[\"PYTHONPATH\"] = f\"{package_path}:{PYTHON_PATH}\"\n\ndocs_path = Path(\"..\").resolve()\nsys.path.insert(1, str(docs_path))\n\nimport unidep  # noqa: E402\n\nproject = \"unidep\"\ncopyright = \"2023, Bas Nijholt\"  # noqa: A001\nauthor = \"Bas Nijholt\"\n\nversion = unidep.__version__\nrelease = unidep.__version__\n\nextensions = [\n    \"sphinx.ext.autodoc\",\n    \"sphinx.ext.autosummary\",\n    \"sphinx.ext.autosectionlabel\",\n    \"sphinx.ext.intersphinx\",\n    \"sphinx.ext.viewcode\",\n    \"sphinx.ext.napoleon\",\n    \"myst_parser\",\n    \"sphinx_autodoc_typehints\",\n]\n\n\nautosectionlabel_maxdepth = 5\nmyst_heading_anchors = 0\ntemplates_path = [\"_templates\"]\nsource_suffix = [\".rst\", \".md\"]\nmaster_doc = \"index\"\nlanguage = \"en\"\npygments_style = \"sphinx\"\nhtml_theme = \"furo\"\nhtml_static_path = [\"_static\"]\nhtmlhelp_basename = \"unidepdoc\"\ndefault_role = \"autolink\"\nintersphinx_mapping = {\n    \"python\": (\"https://docs.python.org/3\", None),\n}\nhtml_logo = \"https://github.com/basnijholt/nijho.lt/raw/2cf0045f9609a176cb53422c591fde946459669d/content/project/unidep/unidep-logo.webp\"\n\n\ndef replace_named_emojis(input_file: Path, output_file: Path) -> None:\n    \"\"\"Replace named emojis in a file with unicode emojis.\"\"\"\n    import emoji\n\n    with input_file.open(\"r\") as infile:\n        content = infile.read()\n    content_with_emojis = emoji.emojize(content, language=\"alias\")\n\n    with output_file.open(\"w\") as outfile:\n        outfile.write(content_with_emojis)\n\n\ndef _change_alerts_to_admonitions(input_text: str) -> str:\n    # Splitting the text into lines\n    lines = input_text.split(\"\\n\")\n\n    # Placeholder for the edited text\n    edited_text = []\n\n    # Mapping of markdown markers to their new format\n    mapping = {\n        \"IMPORTANT\": \"important\",\n        \"NOTE\": \"note\",\n        \"TIP\": \"tip\",\n        \"WARNING\": \"caution\",\n    }\n\n    # Variable to keep track of the current block type\n    current_block_type = None\n\n    for line in lines:\n        # Check if the line starts with any of the markers\n        if any(line.strip().startswith(f\"> [!{marker}]\") for marker in mapping):\n            # Find the marker and set the current block type\n            current_block_type = next(\n                marker for marker in mapping if f\"> [!{marker}]\" in line\n            )\n            # Start of a new block\n            edited_text.append(\"```{\" + mapping[current_block_type] + \"}\")\n        elif current_block_type and line.strip() == \">\":\n            # Empty line within the block, skip it\n            continue\n        elif current_block_type and not line.strip().startswith(\">\"):\n            # End of the current block\n            edited_text.append(\"```\")\n            edited_text.append(line)  # Add the current line as it is\n            current_block_type = None  # Reset the block type\n        elif current_block_type:\n            # Inside the block, so remove '>' and add the line\n            edited_text.append(line.lstrip(\"> \").rstrip())\n        else:\n            # Outside any block, add the line as it is\n            edited_text.append(line)\n\n    # Join the edited lines back into a single string\n    return \"\\n\".join(edited_text)\n\n\ndef change_alerts_to_admonitions(input_file: Path, output_file: Path) -> None:\n    \"\"\"Change markdown alerts to admonitions.\n\n    For example, changes\n    > [!NOTE]\n    > This is a note.\n    to\n    ```{note}\n    This is a note.\n    ```\n    \"\"\"\n    with input_file.open(\"r\") as infile:\n        content = infile.read()\n    new_content = _change_alerts_to_admonitions(content)\n\n    with output_file.open(\"w\") as outfile:\n        outfile.write(new_content)\n\n\ndef replace_example_links(input_file: Path, output_file: Path) -> None:\n    \"\"\"Replace relative links to `example/` files with absolute links to GitHub.\"\"\"\n    with input_file.open(\"r\") as infile:\n        content = infile.read()\n    new_content = content.replace(\n        \"(example/\",\n        \"(https://github.com/basnijholt/unidep/tree/main/example/\",\n    )\n    with output_file.open(\"w\") as outfile:\n        outfile.write(new_content)\n\n\ndef fix_anchors_with_named_emojis(input_file: Path, output_file: Path) -> None:\n    \"\"\"Fix anchors with named emojis.\n\n    WARNING: this currently hardcodes the emojis to remove.\n    \"\"\"\n    to_remove = [\n        \"package\",\n        \"memo\",\n        \"jigsaw\",\n        \"desktop_computer\",\n        \"hammer_and_wrench\",\n        \"warning\",\n    ]\n    with input_file.open(\"r\") as infile:\n        content = infile.read()\n    new_content = content\n    for emoji_name in to_remove:\n        new_content = new_content.replace(f\"#{emoji_name}-\", \"#\")\n    with output_file.open(\"w\") as outfile:\n        outfile.write(new_content)\n\n\ndef normalize_slug(slug: str) -> str:\n    \"\"\"Normalize a slug.\"\"\"\n    return \"#\" + slug[1:].lstrip(\"-\").rstrip(\"-\")\n\n\ndef split_markdown_by_headers(\n    readme_path: Path,\n    out_folder: Path,\n    links: dict[str, str],\n    level: int = 2,\n    to_skip: tuple[str, ...] = (\"Table of Contents\",),\n) -> list[str]:\n    \"\"\"Split a markdown file into individual files based on headers.\"\"\"\n    with readme_path.open(encoding=\"utf-8\") as file:\n        content = file.read()\n\n    # Regex to find second-level headers\n    n = \"#\" * level\n    headers = re.finditer(rf\"\\n({n} .+?)(?=\\n{n} |\\Z)\", content, re.DOTALL)\n\n    # Split content based on headers\n    split_contents: list[str] = []\n    header_contents: list[str] = []\n    start = 0\n    previous_header = \"\"\n    for header in headers:\n        header_title = header.group(1).strip(\"# \").strip()\n        header_contents.append(header_title.split(\"\\n\", 1)[0])\n        end = header.start()\n        if not any(s in previous_header for s in to_skip):\n            split_contents.append(content[start:end].strip())\n        start = end\n        previous_header = header_title\n\n    # Add the last section\n    split_contents.append(content[start:].strip())\n\n    # Create individual files for each section\n    toctree_entries = []\n    for i, (section, header_content) in enumerate(\n        zip(split_contents, header_contents),\n    ):\n        name = (\n            normalize_slug(links[header_content]).lstrip(\"#\")\n            if header_content in links\n            else f\"section_{i}\"\n        )\n        fname = out_folder / f\"{name}.md\"\n        toctree_entries.append(name)\n        with fname.open(\"w\", encoding=\"utf-8\") as file:\n            file.write(section)\n\n    return toctree_entries\n\n\ndef replace_header(file_path: Path, new_header: str) -> None:\n    \"\"\"Replace the first-level header in a markdown file.\"\"\"\n    with file_path.open(\"r\", encoding=\"utf-8\") as file:\n        content = file.read()\n\n    # Find the first-level header (indicated by '# ')\n    # We use a regular expression to match the first occurrence of '# '\n    # and any following characters until a newline\n    content = re.sub(\n        r\"^# .+?\\n\",\n        f\"# {new_header}\\n\",\n        content,\n        count=1,\n        flags=re.MULTILINE,\n    )\n\n    with file_path.open(\"w\", encoding=\"utf-8\") as file:\n        file.write(content)\n\n\ndef extract_toc_links(md_file_path: Path) -> dict[str, str]:\n    \"\"\"Extracts the table of contents with title to link mapping from the given README content.\n\n    Parameters\n    ----------\n    md_file_path\n        Markdown file path.\n\n    Returns\n    -------\n    A dictionary where keys are section titles and values are the corresponding links.\n\n    \"\"\"\n    with md_file_path.open(\"r\") as infile:\n        readme_content = infile.read()\n    toc_start = \"<!-- START doctoc generated TOC please keep comment here to allow auto update -->\"\n    toc_end = \"<!-- END doctoc generated TOC please keep comment here to allow auto update -->\"\n\n    # Extract the TOC section\n    toc_section = re.search(f\"{toc_start}(.*?){toc_end}\", readme_content, re.DOTALL)\n    if not toc_section:\n        msg = \"Table of Contents section not found.\"\n        raise RuntimeError(msg)\n\n    toc_content = toc_section.group(1)\n\n    # Regular expression to match the markdown link syntax\n    link_regex = re.compile(r\"- \\[([^]]+)\\]\\(([^)]+)\\)\")\n\n    # Extracting links\n    return {\n        match.group(1).strip(): match.group(2)\n        for match in link_regex.finditer(toc_content)\n    }\n\n\ndef extract_headers_from_markdown(md_file_path: Path) -> list[tuple[int, str]]:\n    \"\"\"Extracts all headers from a markdown file.\n\n    Parameters\n    ----------\n    md_file_path\n        Path to the markdown file.\n\n    Returns\n    -------\n    A list of tuples containing the level of the header and the header text.\n\n    \"\"\"\n    with md_file_path.open(\"r\") as infile:\n        content = infile.read()\n\n    # Regex to match markdown headers (e.g., ## Header)\n    header_regex = re.compile(r\"^(#+)\\s+(.+)$\", re.MULTILINE)\n\n    # Extract headers\n    return [\n        (len(match.group(1)), match.group(2).strip())\n        for match in header_regex.finditer(content)\n    ]\n\n\ndef replace_links_in_markdown(\n    md_file_path: Path,\n    headers_mapping: dict[str, list[tuple[int, str]]],\n    links: dict[str, str],\n) -> None:\n    \"\"\"Replaces markdown links with updated links that point to the correct file and header anchor.\n\n    Parameters\n    ----------\n    md_file_path\n        Path to the markdown file to process.\n    headers_mapping\n        A dictionary where keys are markdown file names and values are lists of headers.\n    links\n        A dictionary of original header texts mapped to their slug (anchor) in the original README.\n\n    \"\"\"\n    with md_file_path.open(\"r\") as infile:\n        content = infile.read()\n\n    # Replace links based on headers_mapping and links dictionary\n    for file_name, headers in headers_mapping.items():\n        for _header_level, header_text in headers:\n            # Find the original slug for this header text from the links dictionary\n            original_slug = links.get(header_text, \"\")\n            if original_slug:\n                # Remove the '#' from the slug and update the link in the content\n                new_slug = normalize_slug(original_slug)\n                original_slug = original_slug.lstrip(\"#\")\n                content = content.replace(\n                    f\"(#{original_slug})\",\n                    f\"({file_name}{new_slug})\",\n                )\n\n    # Write updated content back to file\n    with md_file_path.open(\"w\") as outfile:\n        outfile.write(content)\n\n\ndef decrease_header_levels(md_file_path: Path) -> None:\n    \"\"\"Decreases the header levels by one in a Markdown file, without going below level 1.\n\n    Parameters\n    ----------\n    md_file_path\n        Path to the Markdown file.\n\n    \"\"\"\n    with md_file_path.open(\"r\", encoding=\"utf-8\") as file:\n        content = file.read()\n\n    # Function to decrease the header level\n    def lower_header_level(match: re.Match) -> str:\n        header_level = len(match.group(1))\n        new_header_level = \"#\" * max(1, header_level - 1)  # Ensure at least one '#'\n        return f\"{new_header_level} {match.group(2)}\"\n\n    # Regular expression for Markdown headers\n    header_regex = re.compile(r\"^(#+)\\s+(.+)$\", re.MULTILINE)\n\n    # Replace headers with decreased levels\n    new_content = header_regex.sub(lower_header_level, content)\n\n    # Write the updated content back to the file\n    with md_file_path.open(\"w\", encoding=\"utf-8\") as file:\n        file.write(new_content)\n\n\ndef write_index_file(docs_path: Path, toctree_entries: list[str]) -> None:\n    \"\"\"Write an index file for the documentation.\"\"\"\n    index_path = docs_path / \"source\" / \"index.md\"\n    # Skip section_0.md as it is renamed to introduction.md\n    pages = \"\\n\".join(f\"{entry}\" for entry in toctree_entries[1:])\n    # Constructing the content using textwrap.dedent for better readability\n    content = textwrap.dedent(\n        \"\"\"\n        ```{{include}} introduction.md\n        ```\n\n        ```{{toctree}}\n        :hidden: true\n        :maxdepth: 2\n        :glob:\n\n        introduction\n        {pages}\n        reference/index\n        ```\n    \"\"\",\n    ).format(pages=pages)\n\n    # Write the content to the file\n    with index_path.open(\"w\", encoding=\"utf-8\") as index_file:\n        index_file.write(content)\n\n\ndef process_readme_for_sphinx_docs(readme_path: Path, docs_path: Path) -> None:\n    \"\"\"Process the README.md file for Sphinx documentation generation.\n\n    Parameters\n    ----------\n    readme_path\n        Path to the original README.md file.\n    docs_path\n        Path to the Sphinx documentation source directory.\n\n    \"\"\"\n    # Step 1: Copy README.md to the Sphinx source directory and apply transformations\n    output_file = docs_path / \"source\" / \"README.md\"\n    replace_named_emojis(readme_path, output_file)\n    change_alerts_to_admonitions(output_file, output_file)\n    replace_example_links(output_file, output_file)\n    fix_anchors_with_named_emojis(output_file, output_file)\n\n    # Step 2: Extract the table of contents links from the processed README\n    links = extract_toc_links(output_file)\n\n    # Step 3: Split the README into individual sections for Sphinx\n    src_folder = docs_path / \"source\"\n    for md_file in src_folder.glob(\"sections_*.md\"):\n        md_file.unlink()\n    toctree_entries = split_markdown_by_headers(output_file, src_folder, links)\n    output_file.unlink()  # Remove the original README file from Sphinx source\n    write_index_file(docs_path, toctree_entries)\n\n    # Step 4: Extract headers from each section for link replacement\n    headers_in_files = {}\n    for md_file in src_folder.glob(\"*.md\"):\n        headers = extract_headers_from_markdown(md_file)\n        decrease_header_levels(md_file)\n        headers_in_files[md_file.name] = headers\n\n    # Rename the first section to 'introduction.md' and update its header\n    shutil.move(src_folder / \"section_0.md\", src_folder / \"introduction.md\")  # type: ignore[arg-type]\n    replace_header(src_folder / \"introduction.md\", new_header=\"🌟 Introduction\")\n\n    # Step 5: Replace links in each markdown file to point to the correct section\n    for md_file in (*src_folder.glob(\"*.md\"), src_folder / \"introduction.md\"):\n        replace_links_in_markdown(md_file, headers_in_files, links)\n\n\nreadme_path = package_path / \"README.md\"\nprocess_readme_for_sphinx_docs(readme_path, docs_path)\n"
  },
  {
    "path": "example/README.md",
    "content": "# Examples\n\n> [!TIP]\n> Try out `unidep` in this folder by running:\n> - `unidep install ./setup_py_project ./hatch_project` to install the `setup_py_project` and `hatch_project` packages and its dependencies with `conda`, then the remaining dependencies with `pip`, and finally the local packages with `pip`\n> - `unidep install-all -e` to install all packages (`setup_py_project`, `hatch_project`, `setuptools_project`, etc.) in editable mode\n> - `unidep conda-lock` to generate a global `conda-lock.yml` file and consistent per package `conda-lock.yml` files\n> - `unidep merge` to merge all `requirements.yaml` files into a single `environment.yaml` file\n> - `unidep pip-compile` to generate a locked `requirements.txt` file\n\nExplore these example projects to understand how `unidep` integrates with different build tools and configurations:\n\n| Project                                            | Build Tool   | `pyproject.toml` | `requirements.yaml` | `setup.py` | Description                                                                        |\n| -------------------------------------------------- | ------------ | ---------------- | ------------------- | ---------- | ---------------------------------------------------------------------------------- |\n| [`setup_py_project`](setup_py_project)             | `setuptools` | ✅                | ✅                   | ✅          | Traditional `setuptools` project with `requirements.yaml`.                         |\n| [`setuptools_project`](setuptools_project)         | `setuptools` | ✅                | ✅                   | ❌          | Modern `setuptools` usage with both `pyproject.toml` and `requirements.yaml`.      |\n| [`pyproject_toml_project`](pyproject_toml_project) | `setuptools` | ✅                | ❌                   | ❌          | Pure `pyproject.toml` setup, showcasing comprehensive dependency management.       |\n| [`hatch_project`](hatch_project)                   | `hatch`      | ✅                | ✅                   | ❌          | Demonstrates `unidep` integration in a Hatchling project with `requirements.yaml`. |\n| [`hatch2_project`](hatch2_project)                 | `hatch`      | ✅                | ❌                   | ❌          | Pure `pyproject.toml` Hatchling project.                                           |\n\n\n## Exploring `unidep` Through Practical Examples\n\n<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->\n\n- [Combine one or multiple `requirements.yaml`/`pyproject.toml` files into a single `environment.yaml` file](#combine-one-or-multiple-requirementsyamlpyprojecttoml-files-into-a-single-environmentyaml-file)\n- [Using `pip install`](#using-pip-install)\n- [Using `unidep install`](#using-unidep-install)\n- [Using `unidep install-all` for installation across multiple projects](#using-unidep-install-all-for-installation-across-multiple-projects)\n\n<!-- END doctoc generated TOC please keep comment here to allow auto update -->\n\n### Combine one or multiple `requirements.yaml`/`pyproject.toml` files into a single `environment.yaml` file\n\nCombine `requirements.yaml` files in subdirectories and into an `environment.yaml` file that can be installed with `conda`.\n\nHere we can just run `unidep merge` with no arguments, since the defaults are the same as what we want.\n\nThis would be the same as running `unidep merge --name myenv --verbose`:\n\n<!-- CODE:BASH:START -->\n<!-- echo '```bash' -->\n<!-- unidep merge --name myenv --verbose -->\n<!-- echo '```' -->\n<!-- CODE:END -->\n<!-- OUTPUT:START -->\n<!-- ⚠️ This content is auto-generated by `markdown-code-runner`. -->\n```bash\n🔍 Scanning in `.` at depth 0\n🔍 Scanning in `hatch2_project` at depth 1\n🔍 Found `\"pyproject.toml\"` with dependencies at `hatch2_project/pyproject.toml`\n🔍 Scanning in `hatch_project` at depth 1\n🔍 Found `\"requirements.yaml\"` at `hatch_project/requirements.yaml`\n🔍 Scanning in `pyproject_toml_project` at depth 1\n🔍 Found `\"pyproject.toml\"` with dependencies at `pyproject_toml_project/pyproject.toml`\n🔍 Scanning in `setup_py_project` at depth 1\n🔍 Found `\"requirements.yaml\"` at `setup_py_project/requirements.yaml`\n🔍 Scanning in `setuptools_project` at depth 1\n🔍 Found `\"requirements.yaml\"` at `setuptools_project/requirements.yaml`\n📄 Parsing `hatch2_project/pyproject.toml`\n📄 Parsing `hatch_project/requirements.yaml`\n📄 Parsing `pyproject_toml_project/pyproject.toml`\n📄 Parsing `../hatch_project[test]` from `local_dependencies`\n📄 Parsing `pyproject_toml_project/../hatch_project/requirements.yaml[test]`\n📄 Moving `test` optional dependencies to main dependencies for `pyproject_toml_project/../hatch_project/requirements.yaml[test]`\n📄 Parsing `setup_py_project/requirements.yaml`\n📄 Parsing `../setuptools_project` from `local_dependencies`\n📄 Parsing `setup_py_project/../setuptools_project/requirements.yaml`\n📄 Parsing `setuptools_project/requirements.yaml`\n📝 Generating environment file at `environment.yaml`\n📝 Environment file generated successfully.\n✅ Generated environment file at `environment.yaml` from `hatch2_project/pyproject.toml`, `hatch_project/requirements.yaml`, `pyproject_toml_project/pyproject.toml`, `setup_py_project/requirements.yaml`, `setuptools_project/requirements.yaml`\n```\n\n<!-- OUTPUT:END -->\n\nSee the resulting [`environment.yaml`](environment.yaml) file which is installable with [`mamba`](https://mamba.readthedocs.io/en/latest/).\nThis file is using `sel(linux|osx|win)` to specify platform specific dependencies.\nAlternatively, use `unidep merge --selector comment` to generate a file that uses comments to specify platform specific dependencies, which can be read by [`conda-lock`](https://github.com/conda/conda-lock).\n\n### Using `pip install`\n\nThis method allows you to install packages defined in a `requirements.yaml` file using `pip`. It focuses on installing only those dependencies that are pip-installable, followed by the local project package.\n\n**How to Use**:\n\n- Run `pip install ./setup_py_project`.\n- This command will process the `requirements.yaml` in the specified directory (`./setup_py_project/`), installing all pip-installable dependencies, including the local project itself.\n\n### Using `unidep install`\n\nUsing `unidep` for installation offers a more comprehensive approach. It handles both Conda and Pip dependencies specified in the `requirements.yaml` file, ensuring all necessary packages are installed, including those not available through pip.\n\n**How to Use**:\n\n- To perform a standard installation, run `unidep install ./setup_py_project`.\n- For an editable installation (useful during development), use `unidep install -e ./setup_py_project`.\n- The `unidep install` command first installs any Conda-specific dependencies from the `requirements.yaml` file, then proceeds to install pip-specific dependencies. Finally, it installs the local project package.\n\n<!-- CODE:BASH:START -->\n<!-- echo '```bash' -->\n<!-- echo '$ unidep install --dry-run -e ./setup_py_project' -->\n<!-- unidep install --dry-run -e ./setup_py_project -->\n<!-- echo '```' -->\n<!-- CODE:END -->\n<!-- OUTPUT:START -->\n<!-- ⚠️ This content is auto-generated by `markdown-code-runner`. -->\n```bash\n$ unidep install --dry-run -e ./setup_py_project\n📦 Installing conda dependencies with `conda install --yes --override-channels --channel conda-forge adaptive\">=0.15.0, <2.0.0\" adaptive-scheduler hpc05 pexpect pfapack numpy\">=1.21\" packaging pandas\">=1,<3\" pytest pytest-cov`\n\n📦 Installing pip dependencies with `/opt/hostedtoolcache/Python/3.14.2/x64/bin/python -m pip install yaml2bib aiokef markdown-code-runner numthreads pyyaml rsync-time-machine slurm-usage unidep`\n\n📝 Found local dependencies: {'setup_py_project': ['hatch_project', 'setuptools_project']}\n\n📦 Installing project with `/opt/hostedtoolcache/Python/3.14.2/x64/bin/python -m pip install --no-deps -e /home/runner/work/unidep/unidep/example/hatch_project -e /home/runner/work/unidep/unidep/example/setuptools_project -e ./setup_py_project`\n\n```\n\n<!-- OUTPUT:END -->\n\n### Using `unidep install-all` for installation across multiple projects\n\nThe `unidep install-all` command provides a convenient way to install all dependencies across multiple projects or packages within a given directory.\nThis command is especially useful in monorepos or when managing several related projects with their own `requirements.yaml` files.\n\n**How `unidep install-all` Works**:\n\n- This command scans a specified directory (or the current directory if none is specified) for `requirements.yaml` files.\n- It then installs dependencies for each found project, handling both Conda and Pip dependencies.\n- The local packages are also installed, making this command a one-stop solution for setting up your entire workspace.\n\n**Usage Examples**:\n\n- Run `unidep install-all` to install all dependencies in the current directory.\n- Use `unidep install-all -e` for an editable install, which is useful during development. This flag ensures that local packages are installed in a way that allows changes to be reflected immediately without needing reinstallation.\n\n**Example Command**:\n\n```bash\n# To install all projects in the current directory in editable mode\nunidep install-all -e\n```\n\n**Output Example**:\n\n<!-- CODE:BASH:START -->\n<!-- echo '```bash' -->\n<!-- echo '$ unidep install-all -e --dry-run' -->\n<!-- unidep install-all -e --dry-run -->\n<!-- echo '```' -->\n<!-- CODE:END -->\n<!-- OUTPUT:START -->\n<!-- ⚠️ This content is auto-generated by `markdown-code-runner`. -->\n```bash\n$ unidep install-all -e --dry-run\n📦 Installing conda dependencies with `conda install --yes --override-channels --channel conda-forge adaptive\">=0.15.0, <2.0.0\" adaptive-scheduler hpc05 pexpect pfapack numpy\">=1.21\" packaging pandas\">=1,<3\" pytest pytest-cov`\n\n📦 Installing pip dependencies with `/opt/hostedtoolcache/Python/3.14.2/x64/bin/python -m pip install yaml2bib aiokef markdown-code-runner numthreads pyyaml rsync-time-machine slurm-usage unidep`\n\n📝 Found local dependencies: {'pyproject_toml_project': ['hatch_project'], 'setup_py_project': ['hatch_project', 'setuptools_project'], 'setuptools_project': ['hatch_project']}\n\n📦 Installing project with `/opt/hostedtoolcache/Python/3.14.2/x64/bin/python -m pip install --no-deps -e ./hatch2_project -e ./hatch_project -e ./pyproject_toml_project -e ./setup_py_project -e ./setuptools_project`\n\n```\n\n<!-- OUTPUT:END -->\n\nThis command streamlines the process of getting a development environment up and running, particularly in complex setups with multiple interdependent projects.\n"
  },
  {
    "path": "example/environment.yaml",
    "content": "# This file is created and managed by `unidep` 3.2.0.\n# For details see https://github.com/basnijholt/unidep\n# File generated with: `unidep merge --name myenv --verbose`\n\nname: myenv\nchannels:\n  - conda-forge\ndependencies:\n  - sel(linux): adaptive >=0.15.0, <2.0.0\n  - sel(linux): adaptive-scheduler\n  - sel(linux): hpc05\n  - sel(linux): pexpect\n  - sel(osx): pexpect\n  - sel(linux): pfapack\n  - numpy >=1.21\n  - packaging\n  - pandas >=1,<3\n  - pytest\n  - pytest-cov\n  - pip:\n    - yaml2bib; sys_platform == 'linux' and platform_machine == 'x86_64'\n    - aiokef\n    - markdown-code-runner\n    - numthreads\n    - pyyaml\n    - rsync-time-machine\n    - slurm-usage\n    - unidep\n    - fileup; sys_platform == 'darwin'\nplatforms:\n  - linux-64\n  - osx-64\n  - osx-arm64\n"
  },
  {
    "path": "example/hatch2_project/README.md",
    "content": "# Hatchling Integration\n\n> [!TIP]\n> - **Standard Installation**: In this example folder, use `pip install .` to install all Python dependencies that are pip-installable, along with the local package itself.\n> - **Comprehensive Installation with `unidep`**: To install all dependencies, including those that are not Python-specific, use `unidep install .`. This command performs the following actions in sequence:\n>   1. `conda install [dependencies from pyproject.toml]` – Installs all Conda installable dependencies.\n>   2. `pip install [dependencies from pyproject.toml]` – Installs remaining pip-only dependencies.\n>   3. `pip install .` – Installs the local package.\n\nFor projects managed with [Hatch](https://hatch.pypa.io/), `unidep` can be configured fully in `pyproject.toml` including all its dependencies.\n\n**Example Configuration for Hatch**:\n\n```toml\n[build-system]\nrequires = [\"hatchling\", \"unidep[toml]\"]  # add \"unidep[toml]\" here\nbuild-backend = \"hatchling.build\"\n\n[project]\ndynamic = [\"dependencies\"]  # add \"dependencies\" here\n# Additional project configurations\n\n[tool.hatch]\n# Additional Hatch configurations\n\n[tool.hatch.metadata]\nallow-direct-references = true  # allow VCS URLs, local paths, etc.\n\n[tool.hatch.metadata.hooks.unidep]  # add this to enable the hook\n\n# Specify pip and conda dependencies here\n[tool.unidep]\nchannels = [\"conda-forge\"]\ndependencies = [\n    { conda = \"adaptive-scheduler:linux64\" },\n    { pip = \"unidep\" },\n    \"numpy >=1.21\",\n    \"hpc05:linux64\",\n    \"pandas >=1,<3\",\n    \"pexpect:unix\",\n    \"wexpect:win64\",\n]\n```\n\n> [!NOTE]\n> See the [`pyproject.toml`](pyproject.toml) for a working example.\n"
  },
  {
    "path": "example/hatch2_project/hatch2_project.py",
    "content": "x = 1\n"
  },
  {
    "path": "example/hatch2_project/pyproject.toml",
    "content": "[build-system]\nrequires = [\"hatchling\", \"unidep[toml]\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\nname = \"hatch2_project\"\ndescription = \"Example hatch2_project for `unidep`.\"\nauthors = [{ name = \"Bas Nijholt\", email = \"bas@nijho.lt\" }]\n# `dependencies` is not needed because it is automatically\n# populated by `unidep` with the dependencies defined in the [tool.unidep] section!\n# dependencies = []\ndynamic = [\"dependencies\"]\nversion = \"0.1.0\"\n\n[tool.hatch]\n\n# Allow direct references (e.g., VCS URLs, local paths) in dependencies\n[tool.hatch.metadata]\nallow-direct-references = true\n\n[tool.hatch.metadata.hooks.unidep]\n\n[tool.unidep]\nchannels = [\"conda-forge\"]\ndependencies = [\n    { conda = \"adaptive-scheduler:linux64\" },\n    { pip = \"unidep\" },\n    \"numpy >=1.21\",\n    \"hpc05:linux64\",\n    \"pandas >=1,<3\",\n    \"pexpect:unix\",\n    \"wexpect:win64\",\n]\n"
  },
  {
    "path": "example/hatch_project/README.md",
    "content": "# Hatchling Integration\n\n> [!TIP]\n> - **Standard Installation**: In this example folder, use `pip install .` to install all Python dependencies that are pip-installable, along with the local package itself.\n> - **Comprehensive Installation with `unidep`**: To install all dependencies, including those that are not Python-specific, use `unidep install .`. This command performs the following actions in sequence:\n>   1. `conda install [dependencies from requirements.yaml]` – Installs all Conda installable dependencies.\n>   2. `pip install [dependencies from requirements.yaml]` – Installs remaining pip-only dependencies.\n>   3. `pip install .` – Installs the local package.\n\nFor projects managed with [Hatch](https://hatch.pypa.io/), `unidep` can be configured in `pyproject.toml` to automatically process `requirements.yaml`.\n\n**Example Configuration for Hatch**:\n\n```toml\n[build-system]\nrequires = [\"hatchling\", \"unidep\"]  # add \"unidep\" here\nbuild-backend = \"hatchling.build\"\n\n[project]\ndynamic = [\"dependencies\"]  # add \"dependencies\" here\n# Additional project configurations\n\n[tool.hatch]\n# Additional Hatch configurations\n\n[tool.hatch.metadata]\nallow-direct-references = true  # allow VCS URLs, local paths, etc.\n\n[tool.hatch.metadata.hooks.unidep]  # add this to enable the hook\n```\n\n> [!NOTE]\n> See the [`pyproject.toml`](pyproject.toml) a working example.\n"
  },
  {
    "path": "example/hatch_project/hatch_project.py",
    "content": "x = 1\n"
  },
  {
    "path": "example/hatch_project/pyproject.toml",
    "content": "[build-system]\nrequires = [\"hatchling\", \"unidep\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\nname = \"hatch_project\"\ndescription = \"Example hatch_project for `unidep`.\"\nauthors = [{ name = \"Bas Nijholt\", email = \"bas@nijho.lt\" }]\n# `dependencies` is not needed because it is automatically\n# populated by `unidep` with the dependencies from the `requirements.yaml`\n# dependencies = []\ndynamic = [\"dependencies\", \"optional-dependencies\"]\nversion = \"0.1.0\"\n\n[tool.hatch]\n\n# Allow direct references (e.g., VCS URLs, local paths) in dependencies\n[tool.hatch.metadata]\nallow-direct-references = true\n\n[tool.hatch.metadata.hooks.unidep]\n"
  },
  {
    "path": "example/hatch_project/requirements.yaml",
    "content": "name: hatch_project\nchannels:\n  - conda-forge\ndependencies:\n  - conda: adaptive-scheduler  # [linux64]\n  - pip: unidep\n  - numpy >=1.21\n  - hpc05  # [linux64]\n  - pandas >=1,<3\n  - pexpect # [unix]\n  - wexpect # [win]\noptional_dependencies:\n  test:\n    - pytest\n    - pytest-cov\n"
  },
  {
    "path": "example/pyproject_toml_project/README.md",
    "content": "# Full `pyproject.toml` integration example\n\n> [!TIP]\n> - **Standard Installation**: In this example folder, use `pip install .` to install all Python dependencies that are pip-installable, along with the local package itself.\n> - **Comprehensive Installation with `unidep`**: To install all dependencies, including those that are not Python-specific, use `unidep install .`. This command performs the following actions in sequence:\n>   1. `conda install [dependencies from pyproject.toml]` – Installs all Conda installable dependencies.\n>   2. `pip install [dependencies from pyproject.toml]` – Installs remaining pip-only dependencies.\n>   3. `pip install .` – Installs the local package.\n\nFor projects using `setuptools` with only a `pyproject.toml` file, configure `unidep` in `pyproject.toml` and specify all dependencies there too.\n\n**Example Configuration for projects using `pyproject.toml`**:\n\nAdd this to `pyproject.toml`:\n\n```toml\n[build-system]\nbuild-backend = \"setuptools.build_meta\"\nrequires = [\"setuptools\", \"unidep[toml]\"]  # add \"unidep[toml]\" here\n\n[project]\ndynamic = [\"dependencies\"]  # add \"dependencies\" here\n\n[tool.unidep]\nchannels = [\"conda-forge\"]\ndependencies = [\n    \"adaptive\",\n    \"pfapack:linux64\",\n    \"packaging\",\n    { pip = \"markdown-code-runner\" },\n    { pip = \"numthreads\" },\n]\n```\n\nThen, of course, add a `requirements.yaml` and you are good to go! 🎉\n\n> [!NOTE]\n> See the [`pyproject.toml`](pyproject.toml) for a working example.\n"
  },
  {
    "path": "example/pyproject_toml_project/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\", \"unidep[toml]\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"pyproject_toml_project\"\ndescription = \"Example pyproject_toml_project for `unidep`.\"\nauthors = [{ name = \"Bas Nijholt\", email = \"bas@nijho.lt\" }]\n# `dependencies` is not needed because it is automatically\n# populated by `unidep` with the dependencies defined in the [tool.unidep] section!\n# dependencies = []\nversion = \"0.1.0\"\ndynamic = [\"dependencies\", \"optional-dependencies\"]\n\n[tool.setuptools]\npy-modules = [\"pyproject_toml_project\"]\n\n[tool.unidep]\nchannels = [\"conda-forge\"]\ndependencies = [\n    \"adaptive:linux64\",\n    \"pfapack:linux64\",\n    \"packaging\",\n    { pip = \"markdown-code-runner\" },\n    { pip = \"numthreads\" },\n]\nlocal_dependencies = [\n    \"../hatch_project[test]\",  # Local dependency with optional dependencies\n]\n[tool.unidep.optional_dependencies]\ndev = [\"mypy\", \"ruff\"]\ntest = [\"pytest\"]\n"
  },
  {
    "path": "example/pyproject_toml_project/pyproject_toml_project.py",
    "content": ""
  },
  {
    "path": "example/setup_py_project/README.md",
    "content": "# `setup.py` integration example\n\n> [!TIP]\n> - **Standard Installation**: In this example folder, use `pip install .` to install all Python dependencies that are pip-installable, along with the local package itself.\n> - **Comprehensive Installation with `unidep`**: To install all dependencies, including those that are not Python-specific, use `unidep install .`. This command performs the following actions in sequence:\n>   1. `conda install [dependencies from requirements.yaml]` – Installs all Conda installable dependencies.\n>   2. `pip install [dependencies from requirements.yaml]` – Installs remaining pip-only dependencies.\n>   3. `pip install .` – Installs the local package.\n\nFor projects using `setuptools` with a `setup.py` file, configure `unidep` in `pyproject.toml` alongside a `requirements.yaml` file.\n\n**Example Configuration for projects using `setup.py`**:\n\nAdd this to `pyproject.toml`:\n\n```toml\n[build-system]\nbuild-backend = \"setuptools.build_meta\"\nrequires = [\"setuptools\", \"unidep\"]\n```\n\nAnd just do not use `install_requires` in `setup.py`.\n\n> [!NOTE]\n> See the [`pyproject.toml`](pyproject.toml) and [`setup.py`](setup.py) for a working example.\n"
  },
  {
    "path": "example/setup_py_project/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\", \"unidep\"]\nbuild-backend = \"setuptools.build_meta\"\n"
  },
  {
    "path": "example/setup_py_project/requirements.yaml",
    "content": "name: setup_py_project\nchannels:\n  - conda-forge\ndependencies:\n  - pandas\n  - adaptive >=0.15.0, <2.0.0  # [linux64]\n  - pip: yaml2bib # [linux64]\n  - pip: rsync-time-machine\n  - pip: slurm-usage\n  - pip: fileup  # [macos]\n  - pip: pyyaml\n  - pip: aiokef\nlocal_dependencies:\n  - ../setuptools_project  # depends on setuptools_project\nplatforms:\n  - linux-64\n  - osx-64\n  - osx-arm64\n"
  },
  {
    "path": "example/setup_py_project/setup.py",
    "content": "from setuptools import setup\n\nsetup(\n    name=\"setup_py_project\",\n    version=\"0.1.0\",\n    description=\"A short description of your package\",\n    py_modules=[\"setup_py_project\"],\n    # This is not needed because `install_requires` is automatically\n    # populated by `unidep` with the dependencies from the `requirements.yaml`\n)\n"
  },
  {
    "path": "example/setup_py_project/setup_py_project.py",
    "content": ""
  },
  {
    "path": "example/setuptools_project/README.md",
    "content": "# Setuptools `pyproject.toml` integration example\n\n> [!TIP]\n> - **Standard Installation**: In this example folder, use `pip install .` to install all Python dependencies that are pip-installable, along with the local package itself.\n> - **Comprehensive Installation with `unidep`**: To install all dependencies, including those that are not Python-specific, use `unidep install .`. This command performs the following actions in sequence:\n>   1. `conda install [dependencies from requirements.yaml]` – Installs all Conda installable dependencies.\n>   2. `pip install [dependencies from requirements.yaml]` – Installs remaining pip-only dependencies.\n>   3. `pip install .` – Installs the local package.\n\nFor projects using `setuptools` with only a `pyproject.toml` file, configure `unidep` in `pyproject.toml` alongside a `requirements.yaml` file.\n\n**Example Configuration for projects using `pyproject.toml`**:\n\nAdd this to `pyproject.toml`:\n\n```toml\n[build-system]\nbuild-backend = \"setuptools.build_meta\"\nrequires = [\"setuptools\", \"unidep\"]  # add \"unidep\" here\n\n[project]\ndynamic = [\"dependencies\"]  # add \"dependencies\" here\n```\n\nThen, of course, add a `requirements.yaml` and you are good to go! 🎉\n\n> [!NOTE]\n> See the [`pyproject.toml`](pyproject.toml) for a working example.\n"
  },
  {
    "path": "example/setuptools_project/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\", \"unidep\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"setuptools_project\"\ndescription = \"Example setuptools_project for `unidep`.\"\nauthors = [{ name = \"Bas Nijholt\", email = \"bas@nijho.lt\" }]\n# `dependencies` is not needed because it is automatically\n# populated by `unidep` with the dependencies from the `requirements.yaml`\n# dependencies = []\nversion = \"0.1.0\"\ndynamic = [\"dependencies\", \"optional-dependencies\"]\n\n[tool.setuptools]\npy-modules = [\"setuptools_project\"]\n"
  },
  {
    "path": "example/setuptools_project/requirements.yaml",
    "content": "name: setuptools_project\nchannels:\n  - conda-forge\ndependencies:\n  - adaptive  # [linux64]\n  - pfapack  # [linux64]\n  - packaging\n  - pip: markdown-code-runner\n  - pip: numthreads\nlocal_dependencies:\n  - ../hatch_project[test]  # depends on hatch_project\noptional_dependencies:\n  dev:\n    - mypy\n    - ruff\n  test:\n    - pytest-xdist\n  setup_py:\n    # Optional local dependency\n    - ../setup_py_project\n"
  },
  {
    "path": "example/setuptools_project/setuptools_project.py",
    "content": ""
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools>=42\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"unidep\"\ndescription = \"Unified Conda and Pip requirements management.\"\ndynamic = [\"version\"]\nauthors = [{ name = \"Bas Nijholt\", email = \"bas@nijho.lt\" }]\ndependencies = [\n    \"packaging\",\n    \"ruamel.yaml\",\n    \"typing_extensions; python_version < '3.8'\",\n    \"tomli; python_version < '3.11'\",\n]\nrequires-python = \">=3.7\"\n\n[project.readme]\nfile = \"README.md\"\ncontent-type = \"text/markdown\"\n\n[project.urls]\nHomepage = \"https://github.com/basnijholt/unidep\"\n\n[project.optional-dependencies]\ntoml = [\"tomli; python_version < '3.11'\"]\nconda-lock = [\"conda-lock\", \"conda-package-handling\"]\npip-compile = [\"pip-tools\"]\npytest = [\"pytest\", \"GitPython\"] # The pytest plugin\nrich = [\"rich-argparse\"]\npixi = [\"pixi-to-conda-lock; python_version >= '3.9'\", \"tomli_w\"]\n# Everything except 'test' and 'docs'\nall = [\n    \"unidep[toml,conda-lock,pip-compile,pytest,rich,pixi]\",\n]\ndocs = [\n    \"myst-parser\",\n    \"sphinx\",\n    \"furo\",\n    \"emoji\",\n    \"sphinx-autodoc-typehints\",\n]\ntest = [\n    \"unidep[all]\",\n    \"tomli_w\",\n    \"pytest\",\n    \"pre-commit\",\n    \"coverage\",\n    \"pytest-cov\",\n    \"pytest-mock\",\n    \"conda-package-handling\",\n    \"rich\",\n]\n\n[project.scripts]\nunidep = \"unidep:_cli.main\"\n\n[project.entry-points.\"setuptools.finalize_distribution_options\"]\nunidep = \"unidep._setuptools_integration:_setuptools_finalizer\"\n\n[project.entry-points.hatch]\nunidep = \"unidep._hatch_integration\"\n\n[project.entry-points.pytest11]\naffected = \"unidep._pytest_plugin\"\n\n[tool.setuptools.packages.find]\ninclude = [\"unidep.*\", \"unidep\"]\n\n[tool.setuptools.dynamic]\nversion = { attr = \"unidep._version.__version__\" }\n\n[tool.setuptools.package-data]\n\"unidep\" = [\"py.typed\"]\n\n[tool.pytest.ini_options]\naddopts = \"\"\"\n    --cov=unidep\n    --cov-report term\n    --cov-report html\n    --cov-report xml\n    --cov-fail-under=100\n    -W error\n    -vvv\n\"\"\"\n\n[tool.coverage.run]\nomit = [\"unidep/_pytest_plugin.py\", \"unidep/_hatch_integration.py\"]\npatch = [\"subprocess\"]\n\n[tool.coverage.report]\nexclude_lines = [\n    \"pragma: no cover\",\n    \"raise NotImplementedError\",\n    \"if TYPE_CHECKING:\",\n    \"if __name__ == .__main__.:\",\n]\n\n[tool.black]\nline_length = 88\n\n[tool.ruff]\nline-length = 88\ntarget-version = \"py37\"\n\n[tool.ruff.lint]\nselect = [\"ALL\"]\nignore = [\n    \"T20\",     # flake8-print\n    \"ANN101\",  # Missing type annotation for {name} in method\n    \"S101\",    # Use of assert detected\n    \"S603\",    # S603 `subprocess` call: check for execution of untrusted input\n    \"PD901\",   # df is a bad variable name. Be kinder to your future self.\n    \"ANN401\",  # Dynamically typed expressions (typing.Any) are disallowed in {name}\n    \"D402\",    # First line should not be the function's signature\n    \"PLW0603\", # Using the global statement to update `X` is discouraged\n    \"D401\",    # First line of docstring should be in imperative mood\n    \"SLF001\",  # Private member accessed\n    \"PLR0913\", # Too many arguments in function definition\n    \"TD002\",   # Missing author in TODO\n]\n\n[tool.ruff.lint.per-file-ignores]\n\"tests/*\" = [\"SLF001\", \"D103\", \"E501\", \"PLR2004\"]\n\"tests/test_examples.py\" = [\"E501\"]\n\".github/*\" = [\"INP001\"]\n\"example/*\" = [\"INP001\", \"D100\"]\n\"docs/*\" = [\"INP001\", \"E501\"]\n\n[tool.ruff.lint.mccabe]\nmax-complexity = 18\n\n[tool.mypy]\npython_version = \"3.8\"  # 3.7 is no longer supported by mypy\n\n# Use bump-my-version, e.g., call `bump-my-version bump minor`\n[tool.bumpversion]\ncurrent_version = \"3.2.0\"\ncommit = true\ncommit_args = \"--no-verify\"\ntag = true\ntag_name = \"v{new_version}\"\n\n[[tool.bumpversion.files]]\nfilename = \"unidep/_version.py\"\nreplace = '__version__ = \"{new_version}\"'\nsearch = '__version__ = \"{current_version}\"'\n"
  },
  {
    "path": "tests/__init__.py",
    "content": "\"\"\"Tests for the ``unidep`` package.\"\"\"\n"
  },
  {
    "path": "tests/helpers.py",
    "content": "\"\"\"unidep tests.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nfrom unidep._dependencies_parsing import yaml_to_toml\n\nif TYPE_CHECKING:\n    import sys\n\n    if sys.version_info >= (3, 8):\n        from typing import Literal\n    else:  # pragma: no cover\n        from typing_extensions import Literal\n\n\nREPO_ROOT = Path(__file__).parent.parent\n\n\ndef maybe_as_toml(toml_or_yaml: Literal[\"toml\", \"yaml\"], p: Path) -> Path:\n    if toml_or_yaml == \"toml\":\n        toml = yaml_to_toml(p)\n        p.unlink()\n        p = p.with_name(\"pyproject.toml\")\n        p.write_text(toml)\n    return p\n"
  },
  {
    "path": "tests/shared_local_install_monorepo/project1/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools>=42\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"project1\"\nversion = \"0.0.1\"\n"
  },
  {
    "path": "tests/shared_local_install_monorepo/project1/requirements.yaml",
    "content": "name: project1\nlocal_dependencies:\n  - ../shared\n"
  },
  {
    "path": "tests/shared_local_install_monorepo/project2/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools>=42\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"project2\"\nversion = \"0.0.1\"\n"
  },
  {
    "path": "tests/shared_local_install_monorepo/project2/requirements.yaml",
    "content": "name: project2\nlocal_dependencies:\n  - ../shared\n"
  },
  {
    "path": "tests/shared_local_install_monorepo/shared/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools>=42\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"shared\"\nversion = \"0.0.1\"\n"
  },
  {
    "path": "tests/shared_local_install_monorepo/shared/requirements.yaml",
    "content": "name: shared\ndependencies: []\n"
  },
  {
    "path": "tests/simple_monorepo/common-requirements.yaml",
    "content": "# This file is uses in the `local_dependencies:` section in `project1/requirements.yml`\n# and `project2/requirements.yml`.\nname: common-requirements\nchannels:\n  - conda-forge\ndependencies:\n  - conda: python_abi\n"
  },
  {
    "path": "tests/simple_monorepo/conda-lock.yml",
    "content": "# This file is created and managed by `unidep` 0.41.0.\n# For details see https://github.com/basnijholt/unidep\n# File generated with: `unidep conda-lock -d tests/simple_monorepo`\n#\n# This environment can be installed with\n# `micromamba create -f conda-lock.yml -n myenv`\n# This file is a `conda-lock` file generated via `unidep`.\n# For details see https://conda.github.io/conda-lock/\n\nversion: 1\nmetadata:\n  content_hash:\n    osx-64: ee56565c906fa861ded63721f99e398fd1734b57368e6f701e25dddf03e7960a\n    osx-arm64: 08362c60bc03c882ae95fa83c4d29e9fb0b7795d63d74ada081ac0fa8a7c69f8\n  channels:\n  - url: conda-forge\n    used_env_vars: []\n  platforms:\n  - osx-64\n  - osx-arm64\n  sources:\n  - tmp.environment.yaml\npackage:\n- name: bzip2\n  version: 1.0.8\n  manager: conda\n  platform: osx-64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h10d778d_5.conda\n  hash:\n    md5: 6097a6ca9ada32699b5fc4312dd6ef18\n    sha256: 61fb2b488928a54d9472113e1280b468a309561caa54f33825a3593da390b242\n  category: main\n  optional: false\n- name: bzip2\n  version: 1.0.8\n  manager: conda\n  platform: osx-arm64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h93a5062_5.conda\n  hash:\n    md5: 1bbc659ca658bfd49a481b5ef7a0f40f\n    sha256: bfa84296a638bea78a8bb29abc493ee95f2a0218775642474a840411b950fe5f\n  category: main\n  optional: false\n- name: python_abi\n  version: '3.12'\n  manager: conda\n  platform: osx-64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.12-4_cp312.conda\n  hash:\n    md5: 87201ac4314b911b74197e588cca3639\n    sha256: 82c154d95c1637604671a02a89e72f1382e89a4269265a03506496bd928f6f14\n  category: main\n  optional: false\n- name: python_abi\n  version: '3.12'\n  manager: conda\n  platform: osx-arm64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.12-4_cp312.conda\n  hash:\n    md5: bbb3a02c78b2d8219d7213f76d644a2a\n    sha256: db25428e4f24f8693ffa39f3ff6dfbb8fd53bc298764b775b57edab1c697560f\n  category: main\n  optional: false\n- name: tzdata\n  version: 2023d\n  manager: conda\n  platform: osx-arm64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/noarch/tzdata-2023d-h0c530f3_0.conda\n  hash:\n    md5: 8dee24b8be2d9ff81e7bd4d7d97ff1b0\n    sha256: 04f2ab3e36f2015841551415bf16bf62933bd94b7085d4be5493b388e95a9c3d\n  category: main\n  optional: false\n"
  },
  {
    "path": "tests/simple_monorepo/project1/conda-lock.yml",
    "content": "# This file is created and managed by `unidep` 0.41.0.\n# For details see https://github.com/basnijholt/unidep\n# File generated with: `unidep conda-lock -d tests/simple_monorepo`\n#\n# This environment can be installed with\n# `micromamba create -f conda-lock.yml -n myenv`\n# This file is a `conda-lock` file generated via `unidep`.\n# For details see https://conda.github.io/conda-lock/\n\nversion: 1\nmetadata:\n  content_hash:\n    osx-64: unidep-is-awesome\n    osx-arm64: unidep-is-awesome\n  channels:\n  - url: conda-forge\n    used_env_vars: []\n  platforms:\n  - osx-64\n  - osx-arm64\n  sources:\n  - tests/simple_monorepo/project1/requirements.yaml\npackage:\n- name: bzip2\n  version: 1.0.8\n  manager: conda\n  platform: osx-64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h10d778d_5.conda\n  hash:\n    md5: 6097a6ca9ada32699b5fc4312dd6ef18\n    sha256: 61fb2b488928a54d9472113e1280b468a309561caa54f33825a3593da390b242\n  category: main\n  optional: false\n- name: bzip2\n  version: 1.0.8\n  manager: conda\n  platform: osx-arm64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h93a5062_5.conda\n  hash:\n    md5: 1bbc659ca658bfd49a481b5ef7a0f40f\n    sha256: bfa84296a638bea78a8bb29abc493ee95f2a0218775642474a840411b950fe5f\n  category: main\n  optional: false\n- name: python_abi\n  version: '3.12'\n  manager: conda\n  platform: osx-64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.12-4_cp312.conda\n  hash:\n    md5: 87201ac4314b911b74197e588cca3639\n    sha256: 82c154d95c1637604671a02a89e72f1382e89a4269265a03506496bd928f6f14\n  category: main\n  optional: false\n- name: python_abi\n  version: '3.12'\n  manager: conda\n  platform: osx-arm64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.12-4_cp312.conda\n  hash:\n    md5: bbb3a02c78b2d8219d7213f76d644a2a\n    sha256: db25428e4f24f8693ffa39f3ff6dfbb8fd53bc298764b775b57edab1c697560f\n  category: main\n  optional: false\n- name: tzdata\n  version: 2023d\n  manager: conda\n  platform: osx-arm64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/noarch/tzdata-2023d-h0c530f3_0.conda\n  hash:\n    md5: 8dee24b8be2d9ff81e7bd4d7d97ff1b0\n    sha256: 04f2ab3e36f2015841551415bf16bf62933bd94b7085d4be5493b388e95a9c3d\n  category: main\n  optional: false\n"
  },
  {
    "path": "tests/simple_monorepo/project1/requirements.yaml",
    "content": "name: project1\nchannels:\n  - conda-forge\ndependencies:\n  - conda: bzip2\nlocal_dependencies:\n  - ../project2  # this means `project2` is a dependency of `project1`\n  - ../common-requirements.yaml\n"
  },
  {
    "path": "tests/simple_monorepo/project2/conda-lock.yml",
    "content": "# This file is created and managed by `unidep` 0.41.0.\n# For details see https://github.com/basnijholt/unidep\n# File generated with: `unidep conda-lock -d tests/simple_monorepo`\n#\n# This environment can be installed with\n# `micromamba create -f conda-lock.yml -n myenv`\n# This file is a `conda-lock` file generated via `unidep`.\n# For details see https://conda.github.io/conda-lock/\n\nversion: 1\nmetadata:\n  content_hash:\n    osx-64: unidep-is-awesome\n    osx-arm64: unidep-is-awesome\n  channels:\n  - url: conda-forge\n    used_env_vars: []\n  platforms:\n  - osx-64\n  - osx-arm64\n  sources:\n  - tests/simple_monorepo/project2/requirements.yaml\npackage:\n- name: python_abi\n  version: '3.12'\n  manager: conda\n  platform: osx-64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.12-4_cp312.conda\n  hash:\n    md5: 87201ac4314b911b74197e588cca3639\n    sha256: 82c154d95c1637604671a02a89e72f1382e89a4269265a03506496bd928f6f14\n  category: main\n  optional: false\n- name: python_abi\n  version: '3.12'\n  manager: conda\n  platform: osx-arm64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.12-4_cp312.conda\n  hash:\n    md5: bbb3a02c78b2d8219d7213f76d644a2a\n    sha256: db25428e4f24f8693ffa39f3ff6dfbb8fd53bc298764b775b57edab1c697560f\n  category: main\n  optional: false\n- name: tzdata\n  version: 2023d\n  manager: conda\n  platform: osx-arm64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/noarch/tzdata-2023d-h0c530f3_0.conda\n  hash:\n    md5: 8dee24b8be2d9ff81e7bd4d7d97ff1b0\n    sha256: 04f2ab3e36f2015841551415bf16bf62933bd94b7085d4be5493b388e95a9c3d\n  category: main\n  optional: false\n"
  },
  {
    "path": "tests/simple_monorepo/project2/requirements.yaml",
    "content": "name: project2\nchannels:\n  - conda-forge\ndependencies:\n  - conda: tzdata  # [arm64]\nlocal_dependencies:\n  - ../common-requirements.yaml\nplatforms:\n  - osx-arm64\n  - osx-64\n"
  },
  {
    "path": "tests/test-pip-and-conda-different-name/conda-lock.yml",
    "content": "# This file is created and managed by `unidep` 0.23.0.\n# For details see https://github.com/basnijholt/unidep\n# File generated with: `unidep conda-lock -d /Users/basnijholt/Code/unidep/tests/test-pip-and-conda-different-name`\n#\n# This environment can be installed with\n# `micromamba create -f conda-lock.yml -n myenv`\n# This file is a `conda-lock` file generated via `unidep`.\n# For details see https://conda.github.io/conda-lock/\n\nversion: 1\nmetadata:\n  content_hash:\n    linux-64: c18392f096a6c21233400900e6ba90c299ad2d28348b69cb62a7cf66734bfe81\n  channels:\n  - url: conda-forge\n    used_env_vars: []\n  platforms:\n  - linux-64\n  sources:\n  - tmp.environment.yaml\npackage:\n- name: _libgcc_mutex\n  version: '0.1'\n  manager: conda\n  platform: linux-64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2\n  hash:\n    md5: d7c89558ba9fa0495403155b64376d81\n    sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726\n  category: main\n  optional: false\n- name: _openmp_mutex\n  version: '4.5'\n  manager: conda\n  platform: linux-64\n  dependencies:\n    _libgcc_mutex: '0.1'\n    libgomp: '>=7.5.0'\n  url: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2\n  hash:\n    md5: 73aaf86a425cc6e73fcf236a5a46396d\n    sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22\n  category: main\n  optional: false\n- name: bzip2\n  version: 1.0.8\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n  url: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda\n  hash:\n    md5: 69b8b6202a07720f448be700e300ccf4\n    sha256: 242c0c324507ee172c0e0dd2045814e746bb303d1eb78870d182ceb0abc726a8\n  category: main\n  optional: false\n- name: ca-certificates\n  version: 2023.11.17\n  manager: conda\n  platform: linux-64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2023.11.17-hbcca054_0.conda\n  hash:\n    md5: 01ffc8d36f9eba0ce0b3c1955fa780ee\n    sha256: fb4b9f4b7d885002db0b93e22f44b5b03791ef3d4efdc9d0662185a0faafd6b6\n  category: main\n  optional: false\n- name: ld_impl_linux-64\n  version: '2.40'\n  manager: conda\n  platform: linux-64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h41732ed_0.conda\n  hash:\n    md5: 7aca3059a1729aa76c597603f10b0dd3\n    sha256: f6cc89d887555912d6c61b295d398cff9ec982a3417d38025c45d5dd9b9e79cd\n  category: main\n  optional: false\n- name: libffi\n  version: 3.4.2\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=9.4.0'\n  url: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2\n  hash:\n    md5: d645c6d2ac96843a2bfaccd2d62b3ac3\n    sha256: ab6e9856c21709b7b517e940ae7028ae0737546122f83c2aa5d692860c3b149e\n  category: main\n  optional: false\n- name: libgcc-ng\n  version: 13.2.0\n  manager: conda\n  platform: linux-64\n  dependencies:\n    _libgcc_mutex: '0.1'\n    _openmp_mutex: '>=4.5'\n  url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_3.conda\n  hash:\n    md5: 23fdf1fef05baeb7eadc2aed5fb0011f\n    sha256: 5e88f658e07a30ab41b154b42c59f079b168acfa9551a75bdc972099453f4105\n  category: main\n  optional: false\n- name: libgomp\n  version: 13.2.0\n  manager: conda\n  platform: linux-64\n  dependencies:\n    _libgcc_mutex: '0.1'\n  url: https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h807b86a_3.conda\n  hash:\n    md5: 7124cbb46b13d395bdde68f2d215c989\n    sha256: 6ebedee39b6bbbc969715d0d7fa4b381cce67e1139862604ffa393f821c08e81\n  category: main\n  optional: false\n- name: libnsl\n  version: 2.0.1\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n  url: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda\n  hash:\n    md5: 30fd6e37fe21f86f4bd26d6ee73eeec7\n    sha256: 26d77a3bb4dceeedc2a41bd688564fe71bf2d149fdcf117049970bc02ff1add6\n  category: main\n  optional: false\n- name: libsqlite\n  version: 3.44.2\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n    libzlib: '>=1.2.13,<1.3.0a0'\n  url: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.44.2-h2797004_0.conda\n  hash:\n    md5: 3b6a9f225c3dbe0d24f4fedd4625c5bf\n    sha256: ee2c4d724a3ed60d5b458864d66122fb84c6ce1df62f735f90d8db17b66cd88a\n  category: main\n  optional: false\n- name: libstdcxx-ng\n  version: 13.2.0\n  manager: conda\n  platform: linux-64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h7e041cc_3.conda\n  hash:\n    md5: 937eaed008f6bf2191c5fe76f87755e9\n    sha256: 6c6c49efedcc5709a66f19fb6b26b69c6a5245310fd1d9a901fd5e38aaf7f882\n  category: main\n  optional: false\n- name: libuuid\n  version: 2.38.1\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n  url: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda\n  hash:\n    md5: 40b61aab5c7ba9ff276c41cfffe6b80b\n    sha256: 787eb542f055a2b3de553614b25f09eefb0a0931b0c87dbcce6efdfd92f04f18\n  category: main\n  optional: false\n- name: libzlib\n  version: 1.2.13\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n  url: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda\n  hash:\n    md5: f36c115f1ee199da648e0597ec2047ad\n    sha256: 370c7c5893b737596fd6ca0d9190c9715d89d888b8c88537ae1ef168c25e82e4\n  category: main\n  optional: false\n- name: msgpack-python\n  version: 1.0.7\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n    libstdcxx-ng: '>=12'\n    python: '>=3.10,<3.11.0a0'\n    python_abi: 3.10.*\n  url: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.0.7-py310hd41b1e2_0.conda\n  hash:\n    md5: dc5263dcaa1347e5a456ead3537be27d\n    sha256: a5c7612029e3871b0af0bd69e8ee1545d3deb93b5bec29cf1bf72522375fda31\n  category: main\n  optional: false\n- name: ncurses\n  version: '6.4'\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n  url: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4-h59595ed_2.conda\n  hash:\n    md5: 7dbaa197d7ba6032caf7ae7f32c1efa0\n    sha256: 91cc03f14caf96243cead96c76fe91ab5925a695d892e83285461fb927dece5e\n  category: main\n  optional: false\n- name: openssl\n  version: 3.2.0\n  manager: conda\n  platform: linux-64\n  dependencies:\n    ca-certificates: ''\n    libgcc-ng: '>=12'\n  url: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.0-hd590300_1.conda\n  hash:\n    md5: 603827b39ea2b835268adb8c821b8570\n    sha256: 80efc6f429bd8e622d999652e5cba2ca56fcdb9c16a439d2ce9b4313116e4a87\n  category: main\n  optional: false\n- name: pip\n  version: 23.3.1\n  manager: conda\n  platform: linux-64\n  dependencies:\n    python: '>=3.7'\n    setuptools: ''\n    wheel: ''\n  url: https://conda.anaconda.org/conda-forge/noarch/pip-23.3.1-pyhd8ed1ab_0.conda\n  hash:\n    md5: 2400c0b86889f43aa52067161e1fb108\n    sha256: 435829a03e1c6009f013f29bb83de8b876c388820bf8cf69a7baeec25f6a3563\n  category: main\n  optional: false\n- name: python\n  version: 3.10.13\n  manager: conda\n  platform: linux-64\n  dependencies:\n    bzip2: '>=1.0.8,<2.0a0'\n    ld_impl_linux-64: '>=2.36.1'\n    libffi: '>=3.4,<4.0a0'\n    libgcc-ng: '>=12'\n    libnsl: '>=2.0.1,<2.1.0a0'\n    libsqlite: '>=3.43.2,<4.0a0'\n    libuuid: '>=2.38.1,<3.0a0'\n    libzlib: '>=1.2.13,<1.3.0a0'\n    ncurses: '>=6.4,<7.0a0'\n    openssl: '>=3.1.4,<4.0a0'\n    readline: '>=8.2,<9.0a0'\n    tk: '>=8.6.13,<8.7.0a0'\n    tzdata: ''\n    xz: '>=5.2.6,<6.0a0'\n  url: https://conda.anaconda.org/conda-forge/linux-64/python-3.10.13-hd12c33a_0_cpython.conda\n  hash:\n    md5: f3a8c32aa764c3e7188b4b810fc9d6ce\n    sha256: a53410f459f314537b379982717b1c5911efc2f0cc26d63c4d6f831bcb31c964\n  category: main\n  optional: false\n- name: python_abi\n  version: '3.10'\n  manager: conda\n  platform: linux-64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.10-4_cp310.conda\n  hash:\n    md5: 26322ec5d7712c3ded99dd656142b8ce\n    sha256: 456bec815bfc2b364763084d08b412fdc4c17eb9ccc66a36cb775fa7ac3cbaec\n  category: main\n  optional: false\n- name: readline\n  version: '8.2'\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n    ncurses: '>=6.3,<7.0a0'\n  url: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda\n  hash:\n    md5: 47d31b792659ce70f470b5c82fdfb7a4\n    sha256: 5435cf39d039387fbdc977b0a762357ea909a7694d9528ab40f005e9208744d7\n  category: main\n  optional: false\n- name: setuptools\n  version: 68.2.2\n  manager: conda\n  platform: linux-64\n  dependencies:\n    python: '>=3.7'\n  url: https://conda.anaconda.org/conda-forge/noarch/setuptools-68.2.2-pyhd8ed1ab_0.conda\n  hash:\n    md5: fc2166155db840c634a1291a5c35a709\n    sha256: 851901b1f8f2049edb36a675f0c3f9a98e1495ef4eb214761b048c6f696a06f7\n  category: main\n  optional: false\n- name: tk\n  version: 8.6.13\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n    libzlib: '>=1.2.13,<1.3.0a0'\n  url: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda\n  hash:\n    md5: d453b98d9c83e71da0741bb0ff4d76bc\n    sha256: e0569c9caa68bf476bead1bed3d79650bb080b532c64a4af7d8ca286c08dea4e\n  category: main\n  optional: false\n- name: tzdata\n  version: 2023c\n  manager: conda\n  platform: linux-64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/noarch/tzdata-2023c-h71feb2d_0.conda\n  hash:\n    md5: 939e3e74d8be4dac89ce83b20de2492a\n    sha256: 0449138224adfa125b220154408419ec37c06b0b49f63c5954724325903ecf55\n  category: main\n  optional: false\n- name: wheel\n  version: 0.42.0\n  manager: conda\n  platform: linux-64\n  dependencies:\n    python: '>=3.7'\n  url: https://conda.anaconda.org/conda-forge/noarch/wheel-0.42.0-pyhd8ed1ab_0.conda\n  hash:\n    md5: 1cdea58981c5cbc17b51973bcaddcea7\n    sha256: 80be0ccc815ce22f80c141013302839b0ed938a2edb50b846cf48d8a8c1cfa01\n  category: main\n  optional: false\n- name: xz\n  version: 5.2.6\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n  url: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2\n  hash:\n    md5: 2161070d867d1b1204ea749c8eec4ef0\n    sha256: 03a6d28ded42af8a347345f82f3eebdd6807a08526d47899a42d62d319609162\n  category: main\n  optional: false\n- name: fluent-logger\n  version: 0.10.0\n  manager: pip\n  platform: linux-64\n  dependencies:\n    msgpack: '>1.0'\n  url: https://files.pythonhosted.org/packages/00/43/9cbd7756dfe2cddc0a76ec2eaec56449ac126455c36fe03ecc86f7feac8f/fluent_logger-0.10.0-py2.py3-none-any.whl\n  hash:\n    sha256: 543637e5e62ec3fc3c92b44e5a4e148a3cea88a0f8ca4fae26c7e60fda7564c1\n  category: main\n  optional: false\n- name: rsync-time-machine\n  version: 1.3.0\n  manager: pip\n  platform: linux-64\n  dependencies: {}\n  url: https://files.pythonhosted.org/packages/42/88/f32647517b00f937c66ae2891f22ebb614ac521386254c2eefd9d770c05e/rsync_time_machine-1.3.0-py3-none-any.whl\n  hash:\n    sha256: 371c23dddddedee51c57dec1f31de82465b9139f17357754dc92269d58c3d454\n  category: main\n  optional: false\n"
  },
  {
    "path": "tests/test-pip-and-conda-different-name/project1/requirements.yaml",
    "content": "name: project2\nchannels:\n  - conda-forge\ndependencies:\n  - conda: python=3.10\n  - pip: fluent-logger  # depends on msgpack, but on conda-forge it's called msgpack-python\n  - pip: rsync-time-machine\nplatforms:\n  - linux-64\n"
  },
  {
    "path": "tests/test-pip-and-conda-different-name/project2/requirements.yaml",
    "content": "name: project2\nchannels:\n  - conda-forge\ndependencies:\n  - conda: msgpack-python\nplatforms:\n  - linux-64\n"
  },
  {
    "path": "tests/test-pip-package-with-conda-dependency/conda-lock.yml",
    "content": "# This file is created and managed by `unidep` 0.23.0.\n# For details see https://github.com/basnijholt/unidep\n# File generated with: `unidep conda-lock -d tests/test-pip-package-with-conda-dependency`\n#\n# This environment can be installed with\n# `micromamba create -f conda-lock.yml -n myenv`\n# This file is a `conda-lock` file generated via `unidep`.\n# For details see https://conda.github.io/conda-lock/\n\nversion: 1\nmetadata:\n  content_hash:\n    linux-64: 64492feacfc7d0ed4ee041529c75ad1ec9543bb69603d7519427014d47061f9a\n  channels:\n  - url: conda-forge\n    used_env_vars: []\n  platforms:\n  - linux-64\n  sources:\n  - tmp.environment.yaml\npackage:\n- name: _libgcc_mutex\n  version: '0.1'\n  manager: conda\n  platform: linux-64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2\n  hash:\n    md5: d7c89558ba9fa0495403155b64376d81\n    sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726\n  category: main\n  optional: false\n- name: _openmp_mutex\n  version: '4.5'\n  manager: conda\n  platform: linux-64\n  dependencies:\n    _libgcc_mutex: '0.1'\n    libgomp: '>=7.5.0'\n  url: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2\n  hash:\n    md5: 73aaf86a425cc6e73fcf236a5a46396d\n    sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22\n  category: main\n  optional: false\n- name: bzip2\n  version: 1.0.8\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n  url: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda\n  hash:\n    md5: 69b8b6202a07720f448be700e300ccf4\n    sha256: 242c0c324507ee172c0e0dd2045814e746bb303d1eb78870d182ceb0abc726a8\n  category: main\n  optional: false\n- name: ca-certificates\n  version: 2023.11.17\n  manager: conda\n  platform: linux-64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2023.11.17-hbcca054_0.conda\n  hash:\n    md5: 01ffc8d36f9eba0ce0b3c1955fa780ee\n    sha256: fb4b9f4b7d885002db0b93e22f44b5b03791ef3d4efdc9d0662185a0faafd6b6\n  category: main\n  optional: false\n- name: ld_impl_linux-64\n  version: '2.40'\n  manager: conda\n  platform: linux-64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h41732ed_0.conda\n  hash:\n    md5: 7aca3059a1729aa76c597603f10b0dd3\n    sha256: f6cc89d887555912d6c61b295d398cff9ec982a3417d38025c45d5dd9b9e79cd\n  category: main\n  optional: false\n- name: libexpat\n  version: 2.5.0\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n  url: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.5.0-hcb278e6_1.conda\n  hash:\n    md5: 6305a3dd2752c76335295da4e581f2fd\n    sha256: 74c98a563777ae2ad71f1f74d458a8ab043cee4a513467c159ccf159d0e461f3\n  category: main\n  optional: false\n- name: libffi\n  version: 3.4.2\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=9.4.0'\n  url: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2\n  hash:\n    md5: d645c6d2ac96843a2bfaccd2d62b3ac3\n    sha256: ab6e9856c21709b7b517e940ae7028ae0737546122f83c2aa5d692860c3b149e\n  category: main\n  optional: false\n- name: libgcc-ng\n  version: 13.2.0\n  manager: conda\n  platform: linux-64\n  dependencies:\n    _libgcc_mutex: '0.1'\n    _openmp_mutex: '>=4.5'\n  url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_3.conda\n  hash:\n    md5: 23fdf1fef05baeb7eadc2aed5fb0011f\n    sha256: 5e88f658e07a30ab41b154b42c59f079b168acfa9551a75bdc972099453f4105\n  category: main\n  optional: false\n- name: libgomp\n  version: 13.2.0\n  manager: conda\n  platform: linux-64\n  dependencies:\n    _libgcc_mutex: '0.1'\n  url: https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h807b86a_3.conda\n  hash:\n    md5: 7124cbb46b13d395bdde68f2d215c989\n    sha256: 6ebedee39b6bbbc969715d0d7fa4b381cce67e1139862604ffa393f821c08e81\n  category: main\n  optional: false\n- name: libnsl\n  version: 2.0.1\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n  url: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda\n  hash:\n    md5: 30fd6e37fe21f86f4bd26d6ee73eeec7\n    sha256: 26d77a3bb4dceeedc2a41bd688564fe71bf2d149fdcf117049970bc02ff1add6\n  category: main\n  optional: false\n- name: libsqlite\n  version: 3.44.2\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n    libzlib: '>=1.2.13,<1.3.0a0'\n  url: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.44.2-h2797004_0.conda\n  hash:\n    md5: 3b6a9f225c3dbe0d24f4fedd4625c5bf\n    sha256: ee2c4d724a3ed60d5b458864d66122fb84c6ce1df62f735f90d8db17b66cd88a\n  category: main\n  optional: false\n- name: libstdcxx-ng\n  version: 13.2.0\n  manager: conda\n  platform: linux-64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h7e041cc_3.conda\n  hash:\n    md5: 937eaed008f6bf2191c5fe76f87755e9\n    sha256: 6c6c49efedcc5709a66f19fb6b26b69c6a5245310fd1d9a901fd5e38aaf7f882\n  category: main\n  optional: false\n- name: libuuid\n  version: 2.38.1\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n  url: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda\n  hash:\n    md5: 40b61aab5c7ba9ff276c41cfffe6b80b\n    sha256: 787eb542f055a2b3de553614b25f09eefb0a0931b0c87dbcce6efdfd92f04f18\n  category: main\n  optional: false\n- name: libzlib\n  version: 1.2.13\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n  url: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda\n  hash:\n    md5: f36c115f1ee199da648e0597ec2047ad\n    sha256: 370c7c5893b737596fd6ca0d9190c9715d89d888b8c88537ae1ef168c25e82e4\n  category: main\n  optional: false\n- name: ncurses\n  version: '6.4'\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n  url: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4-h59595ed_2.conda\n  hash:\n    md5: 7dbaa197d7ba6032caf7ae7f32c1efa0\n    sha256: 91cc03f14caf96243cead96c76fe91ab5925a695d892e83285461fb927dece5e\n  category: main\n  optional: false\n- name: openssl\n  version: 3.2.0\n  manager: conda\n  platform: linux-64\n  dependencies:\n    ca-certificates: ''\n    libgcc-ng: '>=12'\n  url: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.0-hd590300_1.conda\n  hash:\n    md5: 603827b39ea2b835268adb8c821b8570\n    sha256: 80efc6f429bd8e622d999652e5cba2ca56fcdb9c16a439d2ce9b4313116e4a87\n  category: main\n  optional: false\n- name: pip\n  version: 23.3.1\n  manager: conda\n  platform: linux-64\n  dependencies:\n    python: '>=3.7'\n    setuptools: ''\n    wheel: ''\n  url: https://conda.anaconda.org/conda-forge/noarch/pip-23.3.1-pyhd8ed1ab_0.conda\n  hash:\n    md5: 2400c0b86889f43aa52067161e1fb108\n    sha256: 435829a03e1c6009f013f29bb83de8b876c388820bf8cf69a7baeec25f6a3563\n  category: main\n  optional: false\n- name: pybind11\n  version: 2.11.1\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n    libstdcxx-ng: '>=12'\n    pybind11-global: 2.11.1\n    python: '>=3.11,<3.12.0a0'\n    python_abi: 3.11.*\n  url: https://conda.anaconda.org/conda-forge/linux-64/pybind11-2.11.1-py311h9547e67_2.conda\n  hash:\n    md5: 64a8933c635a78a6dc0f0cb07ef19a6e\n    sha256: 98ea0d8edd21b6ef7205aeafa6dbdcb1829aeb888ec8a4ba69d58effb912d536\n  category: main\n  optional: false\n- name: pybind11-global\n  version: 2.11.1\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n    libstdcxx-ng: '>=12'\n    python: '>=3.11,<3.12.0a0'\n    python_abi: 3.11.*\n  url: https://conda.anaconda.org/conda-forge/linux-64/pybind11-global-2.11.1-py311h9547e67_2.conda\n  hash:\n    md5: 71330b362711dd503ef2e8139570b8e0\n    sha256: 6f231d62f03e99c0e45d70f17a82c0482dbe8286412fe44556bcfeccbacd5c0c\n  category: main\n  optional: false\n- name: python\n  version: 3.11.6\n  manager: conda\n  platform: linux-64\n  dependencies:\n    bzip2: '>=1.0.8,<2.0a0'\n    ld_impl_linux-64: '>=2.36.1'\n    libexpat: '>=2.5.0,<3.0a0'\n    libffi: '>=3.4,<4.0a0'\n    libgcc-ng: '>=12'\n    libnsl: '>=2.0.0,<2.1.0a0'\n    libsqlite: '>=3.43.0,<4.0a0'\n    libuuid: '>=2.38.1,<3.0a0'\n    libzlib: '>=1.2.13,<1.3.0a0'\n    ncurses: '>=6.4,<7.0a0'\n    openssl: '>=3.1.3,<4.0a0'\n    readline: '>=8.2,<9.0a0'\n    tk: '>=8.6.13,<8.7.0a0'\n    tzdata: ''\n    xz: '>=5.2.6,<6.0a0'\n  url: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.6-hab00c5b_0_cpython.conda\n  hash:\n    md5: b0dfbe2fcbfdb097d321bfd50ecddab1\n    sha256: 84f13bd70cff5dcdaee19263b2d4291d5793856a718efc1b63a9cfa9eb6e2ca1\n  category: main\n  optional: false\n- name: python_abi\n  version: '3.11'\n  manager: conda\n  platform: linux-64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.11-4_cp311.conda\n  hash:\n    md5: d786502c97404c94d7d58d258a445a65\n    sha256: 0be3ac1bf852d64f553220c7e6457e9c047dfb7412da9d22fbaa67e60858b3cf\n  category: main\n  optional: false\n- name: readline\n  version: '8.2'\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n    ncurses: '>=6.3,<7.0a0'\n  url: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda\n  hash:\n    md5: 47d31b792659ce70f470b5c82fdfb7a4\n    sha256: 5435cf39d039387fbdc977b0a762357ea909a7694d9528ab40f005e9208744d7\n  category: main\n  optional: false\n- name: setuptools\n  version: 68.2.2\n  manager: conda\n  platform: linux-64\n  dependencies:\n    python: '>=3.7'\n  url: https://conda.anaconda.org/conda-forge/noarch/setuptools-68.2.2-pyhd8ed1ab_0.conda\n  hash:\n    md5: fc2166155db840c634a1291a5c35a709\n    sha256: 851901b1f8f2049edb36a675f0c3f9a98e1495ef4eb214761b048c6f696a06f7\n  category: main\n  optional: false\n- name: tk\n  version: 8.6.13\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n    libzlib: '>=1.2.13,<1.3.0a0'\n  url: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda\n  hash:\n    md5: d453b98d9c83e71da0741bb0ff4d76bc\n    sha256: e0569c9caa68bf476bead1bed3d79650bb080b532c64a4af7d8ca286c08dea4e\n  category: main\n  optional: false\n- name: tzdata\n  version: 2023c\n  manager: conda\n  platform: linux-64\n  dependencies: {}\n  url: https://conda.anaconda.org/conda-forge/noarch/tzdata-2023c-h71feb2d_0.conda\n  hash:\n    md5: 939e3e74d8be4dac89ce83b20de2492a\n    sha256: 0449138224adfa125b220154408419ec37c06b0b49f63c5954724325903ecf55\n  category: main\n  optional: false\n- name: wheel\n  version: 0.42.0\n  manager: conda\n  platform: linux-64\n  dependencies:\n    python: '>=3.7'\n  url: https://conda.anaconda.org/conda-forge/noarch/wheel-0.42.0-pyhd8ed1ab_0.conda\n  hash:\n    md5: 1cdea58981c5cbc17b51973bcaddcea7\n    sha256: 80be0ccc815ce22f80c141013302839b0ed938a2edb50b846cf48d8a8c1cfa01\n  category: main\n  optional: false\n- name: xz\n  version: 5.2.6\n  manager: conda\n  platform: linux-64\n  dependencies:\n    libgcc-ng: '>=12'\n  url: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2\n  hash:\n    md5: 2161070d867d1b1204ea749c8eec4ef0\n    sha256: 03a6d28ded42af8a347345f82f3eebdd6807a08526d47899a42d62d319609162\n  category: main\n  optional: false\n- name: cutde\n  version: 23.6.25\n  manager: pip\n  platform: linux-64\n  dependencies:\n    mako: '*'\n    pybind11: '*'\n  url: https://files.pythonhosted.org/packages/08/15/0ae45db8fcc0d2da6002d13900689e2fe7773da038922b1ff450ab08088e/cutde-23.6.25.tar.gz\n  hash:\n    sha256: 946aeb03b3bf2f9060dabda1dd84330a67a7fddab27879010107382bcca31eac\n  category: main\n  optional: false\n- name: mako\n  version: 1.3.0\n  manager: pip\n  platform: linux-64\n  dependencies:\n    markupsafe: '>=0.9.2'\n  url: https://files.pythonhosted.org/packages/24/3b/11fe92d68c6a42468ddab0cf03f454419b0788fff4e91ba46b8bebafeffd/Mako-1.3.0-py3-none-any.whl\n  hash:\n    sha256: 57d4e997349f1a92035aa25c17ace371a4213f2ca42f99bee9a602500cfd54d9\n  category: main\n  optional: false\n- name: markupsafe\n  version: 2.1.3\n  manager: pip\n  platform: linux-64\n  dependencies: {}\n  url: https://files.pythonhosted.org/packages/fe/21/2eff1de472ca6c99ec3993eab11308787b9879af9ca8bbceb4868cf4f2ca/MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\n  hash:\n    sha256: bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2\n  category: main\n  optional: false\n- name: rsync-time-machine\n  version: 1.3.0\n  manager: pip\n  platform: linux-64\n  dependencies: {}\n  url: https://files.pythonhosted.org/packages/42/88/f32647517b00f937c66ae2891f22ebb614ac521386254c2eefd9d770c05e/rsync_time_machine-1.3.0-py3-none-any.whl\n  hash:\n    sha256: 371c23dddddedee51c57dec1f31de82465b9139f17357754dc92269d58c3d454\n  category: main\n  optional: false\n"
  },
  {
    "path": "tests/test-pip-package-with-conda-dependency/project1/requirements.yaml",
    "content": "name: project1\nchannels:\n  - conda-forge\ndependencies:\n  - pybind11\nplatforms:\n  - linux-64\n"
  },
  {
    "path": "tests/test-pip-package-with-conda-dependency/project2/requirements.yaml",
    "content": "name: project2\nchannels:\n  - conda-forge\ndependencies:\n  - conda: python=3.11\n  - pip: cutde  # depends on pybind11, but pybind11 is installed via conda because project1/\n  - pip: rsync-time-machine\nplatforms:\n  - linux-64\n"
  },
  {
    "path": "tests/test_cli.py",
    "content": "\"\"\"unidep CLI tests.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport platform\nimport re\nimport shutil\nimport subprocess\nimport sys\nimport textwrap\nfrom contextlib import contextmanager\nfrom pathlib import Path\nfrom typing import Any, Generator\nfrom unittest.mock import patch\n\nimport pytest\n\ntry:\n    import tomllib\nexcept ImportError:  # pragma: no cover\n    import tomli as tomllib\n\nfrom unidep._cli import (\n    CondaExecutable,\n    _capitalize_dir,\n    _collect_available_optional_dependency_groups,\n    _collect_selected_conda_like_platforms,\n    _conda_env_list,\n    _conda_info,\n    _conda_root_prefix,\n    _find_windows_path,\n    _flatten_selected_dependency_entries,\n    _get_conda_executable,\n    _identify_conda_executable,\n    _install_all_command,\n    _install_command,\n    _maybe_conda_run,\n    _maybe_create_conda_env_args,\n    _merge_command,\n    _merge_optional_dependency_extras,\n    _pip_compile_command,\n    _pip_subcommand,\n    _print_versions,\n)\nfrom unidep._dependencies_parsing import parse_requirements\n\nREPO_ROOT = Path(__file__).parent.parent\n\nEXAMPLE_PROJECTS = [\n    \"setup_py_project\",\n    \"setuptools_project\",\n    \"hatch_project\",\n    \"pyproject_toml_project\",\n    \"hatch2_project\",\n]\n\n\ndef current_env_and_prefix() -> tuple[str, Path]:\n    \"\"\"Get the current conda environment name and prefix.\"\"\"\n    try:\n        prefix = _conda_root_prefix(\"conda\")\n    except (KeyError, FileNotFoundError):\n        prefix = _conda_root_prefix(\"micromamba\")\n    folder, env_name = Path(os.environ[\"CONDA_PREFIX\"]).parts[-2:]\n    if folder != \"envs\":\n        return \"base\", prefix\n    return env_name, prefix / \"envs\" / env_name\n\n\n@pytest.mark.parametrize(\n    \"project\",\n    EXAMPLE_PROJECTS,\n)\ndef test_install_command(project: str, capsys: pytest.CaptureFixture) -> None:\n    current_env, prefix = current_env_and_prefix()\n    print(f\"current_env: {current_env}, prefix: {prefix}\")\n    for kw in [\n        {\"conda_env_name\": current_env, \"conda_env_prefix\": None},\n        {\"conda_env_name\": None, \"conda_env_prefix\": prefix},\n    ]:\n        _install_command(\n            REPO_ROOT / \"example\" / project,\n            conda_executable=\"\",  # type: ignore[arg-type]\n            conda_lock_file=None,\n            dry_run=True,\n            editable=False,\n            verbose=True,\n            **kw,  # type: ignore[arg-type]\n        )\n        captured = capsys.readouterr()\n        assert \"Installing conda dependencies\" in captured.out\n        assert \"Installing pip dependencies\" in captured.out\n        assert \"Installing project with\" in captured.out\n\n\n@pytest.mark.parametrize(\n    \"project\",\n    EXAMPLE_PROJECTS,\n)\ndef test_unidep_install_dry_run(project: str) -> None:\n    # Path to the requirements file\n    requirements_path = REPO_ROOT / \"example\" / project\n\n    # Ensure the requirements file exists\n    assert requirements_path.exists(), \"Requirements file does not exist\"\n\n    # Run the unidep install command\n    result = subprocess.run(\n        [  # noqa: S607\n            \"unidep\",\n            \"install\",\n            \"--dry-run\",\n            str(requirements_path),\n        ],\n        check=True,\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n    )\n\n    # Check the output\n    assert result.returncode == 0, \"Command failed to execute successfully\"\n    if project in (\"setup_py_project\", \"setuptools_project\"):\n        assert \"📦 Installing conda dependencies with\" in result.stdout\n    assert \"📦 Installing pip dependencies with\" in result.stdout\n    assert \"📦 Installing project with\" in result.stdout\n\n\ndef test_install_all_command(capsys: pytest.CaptureFixture) -> None:\n    _install_all_command(\n        conda_executable=\"\",  # type: ignore[arg-type]\n        conda_env_name=None,\n        conda_env_prefix=None,\n        conda_lock_file=None,\n        dry_run=True,\n        editable=True,\n        directory=REPO_ROOT / \"example\",\n        depth=1,\n        verbose=False,\n    )\n    captured = capsys.readouterr()\n    assert \"Installing conda dependencies\" in captured.out\n    assert \"Installing pip dependencies\" in captured.out\n    projects = [REPO_ROOT / \"example\" / p for p in EXAMPLE_PROJECTS]\n    pkgs = \" \".join([f\"-e {p}\" for p in sorted(projects)])\n    assert f\"pip install --no-deps {pkgs}`\" in captured.out\n\n\ndef test_install_command_deduplicates_shared_local_dependencies(\n    tmp_path: Path,\n    capsys: pytest.CaptureFixture,\n) -> None:\n    fixture_root = REPO_ROOT / \"tests\" / \"shared_local_install_monorepo\"\n    monorepo = tmp_path / fixture_root.name\n    shutil.copytree(fixture_root, monorepo)\n    shared = monorepo / \"shared\"\n    project1 = monorepo / \"project1\"\n    project2 = monorepo / \"project2\"\n\n    _install_command(\n        project1,\n        project2,\n        conda_executable=\"\",  # type: ignore[arg-type]\n        conda_env_name=None,\n        conda_env_prefix=None,\n        conda_lock_file=None,\n        dry_run=True,\n        editable=True,\n        no_dependencies=True,\n        no_uv=True,\n        verbose=False,\n    )\n\n    captured = capsys.readouterr()\n    pkgs = \" \".join([f\"-e {p}\" for p in sorted((project1, project2, shared))])\n    assert f\"pip install --no-deps {pkgs}`\" in captured.out\n    assert captured.out.count(f\"-e {shared}\") == 1\n\n\ndef mock_uv_env(tmp_path: Path) -> dict[str, str]:\n    \"\"\"Create a mock uv executable and return env with it in the PATH.\"\"\"\n    mock_uv_path = tmp_path / (\"uv.bat\" if platform.system() == \"Windows\" else \"uv\")\n    if platform.system() == \"Windows\":\n        mock_uv_path.write_text(\"@echo off\\necho Mock uv called %*\")\n    else:\n        mock_uv_path.write_text(\"#!/bin/sh\\necho 'Mock uv called' \\\"$@\\\"\")\n    mock_uv_path.chmod(0o755)  # Make it executable\n\n    # Add tmp_path to the PATH environment variable\n    env = os.environ.copy()\n    env[\"PATH\"] = f\"{tmp_path}{os.pathsep}{env['PATH']}\"\n    return env\n\n\n@pytest.mark.parametrize(\"with_uv\", [True, False])\ndef test_unidep_install_all_dry_run(tmp_path: Path, with_uv: bool) -> None:  # noqa: FBT001\n    # Path to the requirements file\n    requirements_path = REPO_ROOT / \"example\"\n\n    # Ensure the requirements file exists\n    assert requirements_path.exists(), \"Requirements file does not exist\"\n\n    # Run the unidep install command\n    result = subprocess.run(\n        [  # noqa: S607\n            \"unidep\",\n            \"install-all\",\n            \"--dry-run\",\n            \"--editable\",\n            \"--directory\",\n            str(requirements_path),\n            *([\"--no-uv\"] if not with_uv else []),\n        ],\n        check=True,\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n        env=mock_uv_env(tmp_path) if with_uv else None,\n    )\n\n    # Check the output\n    assert result.returncode == 0, \"Command failed to execute successfully\"\n    assert \"📦 Installing conda dependencies with `\" in result.stdout\n\n    assert r\"📦 Installing pip dependencies with `\" in result.stdout\n    assert (\n        \"📝 Found local dependencies: {'pyproject_toml_project': ['hatch_project'], 'setup_py_project': ['hatch_project', 'setuptools_project'], 'setuptools_project': ['hatch_project']}\"\n        in result.stdout\n    )\n    projects = [REPO_ROOT / \"example\" / p for p in EXAMPLE_PROJECTS]\n    pkgs = \" \".join([f\"-e {p}\" for p in sorted(projects)])\n    assert \"📦 Installing project with `\" in result.stdout\n    if with_uv:\n        assert \"uv pip install --python\" in result.stdout\n    else:\n        assert f\" -m pip install --no-deps {pkgs}\" in result.stdout\n\n\ndef test_unidep_conda() -> None:\n    # Path to the requirements file\n    requirements_path = REPO_ROOT / \"example\" / \"setup_py_project\"\n\n    assert requirements_path.exists(), \"Requirements file does not exist\"\n\n    result = subprocess.run(\n        [  # noqa: S607\n            \"unidep\",\n            \"conda\",\n            \"--file\",\n            str(requirements_path),\n        ],\n        check=True,\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n    )\n\n    # Check the output\n    assert result.returncode == 0, \"Command failed to execute successfully\"\n    assert \"pandas\" in result.stdout\n\n\ndef test_unidep_pixi_cli_respects_overrides(tmp_path: Path) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            channels:\n              - conda-forge\n            dependencies:\n              - numpy >=1.20\n              - pandas >=2.0\n              - scipy <1.10\n              - pyobjc  # [osx]\n            platforms:\n              - linux-64\n              - osx-arm64\n            \"\"\",\n        ),\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    result = subprocess.run(\n        [  # noqa: S607\n            \"unidep\",\n            \"pixi\",\n            \"--file\",\n            str(req_file),\n            \"--output\",\n            str(output_file),\n            \"--name\",\n            \"test-project\",\n            \"--platform\",\n            \"linux-64\",\n            \"--ignore-pin\",\n            \"numpy\",\n            \"--skip-dependency\",\n            \"pandas\",\n            \"--overwrite-pin\",\n            \"scipy>=1.11\",\n        ],\n        check=True,\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n    )\n\n    assert result.returncode == 0, \"Command failed to execute successfully\"\n    with output_file.open(\"rb\") as f:\n        data = tomllib.load(f)\n\n    deps = data[\"dependencies\"]\n    assert deps[\"numpy\"] == \"*\"\n    assert \"pandas\" not in deps\n    assert deps[\"scipy\"] == \">=1.11\"\n    assert data[\"workspace\"][\"platforms\"] == [\"linux-64\"]\n    assert \"target\" not in data or \"osx-arm64\" not in data[\"target\"]\n\n\ndef test_unidep_pixi_cli_channel_override(tmp_path: Path) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            channels:\n              - conda-forge\n            dependencies:\n              - numpy\n            platforms:\n              - linux-64\n            \"\"\",\n        ),\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    result = subprocess.run(\n        [  # noqa: S607\n            \"unidep\",\n            \"pixi\",\n            \"--file\",\n            str(req_file),\n            \"--output\",\n            str(output_file),\n            \"--channel\",\n            \"defaults\",\n            \"--channel\",\n            \"bioconda\",\n        ],\n        check=True,\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n    )\n\n    assert result.returncode == 0\n    with output_file.open(\"rb\") as f:\n        data = tomllib.load(f)\n\n    assert data[\"workspace\"][\"channels\"] == [\"defaults\", \"bioconda\"]\n\n\ndef test_unidep_pixi_cli_ranged_build_string(tmp_path: Path) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            channels:\n              - conda-forge\n            dependencies:\n              - conda: numpy >=1.20,<1.21 py310*\n            platforms:\n              - linux-64\n            \"\"\",\n        ),\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    result = subprocess.run(\n        [  # noqa: S607\n            \"unidep\",\n            \"pixi\",\n            \"--file\",\n            str(req_file),\n            \"--output\",\n            str(output_file),\n        ],\n        check=True,\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n    )\n\n    assert result.returncode == 0, \"Command failed to execute successfully\"\n    with output_file.open(\"rb\") as f:\n        data = tomllib.load(f)\n\n    numpy_spec = data[\"dependencies\"][\"numpy\"]\n    assert numpy_spec[\"version\"] == \">=1.20,<1.21\"\n    assert numpy_spec[\"build\"] == \"py310*\"\n\n\ndef test_merge_uses_selector_platforms_when_no_platforms_declared(\n    tmp_path: Path,\n) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            channels:\n              - conda-forge\n            dependencies:\n              - cuda-toolkit  # [linux64]\n            \"\"\",\n        ),\n    )\n    output_file = tmp_path / \"environment.yaml\"\n\n    with patch(\"unidep.utils.identify_current_platform\", return_value=\"osx-arm64\"):\n        _merge_command(\n            depth=1,\n            directory=tmp_path,\n            files=[req_file],\n            name=\"myenv\",\n            output=output_file,\n            stdout=False,\n            selector=\"comment\",\n            platforms=[],\n            optional_dependencies=[],\n            all_optional_dependencies=False,\n            ignore_pins=[],\n            skip_dependencies=[],\n            overwrite_pins=[],\n            verbose=False,\n        )\n\n    content = output_file.read_text()\n    assert \"platforms:\" in content\n    assert \"  - linux-64\" in content\n    assert \"  - osx-arm64\" not in content\n\n\n@pytest.mark.parametrize(\n    (\n        \"content\",\n        \"current_platform\",\n        \"expected_dependency\",\n        \"expected_platforms\",\n        \"excluded_platform\",\n    ),\n    [\n        (\n            \"\"\"\\\n            dependencies:\n              - conda: click >=8\n              - pip: click  # [osx]\n            \"\"\",\n            \"linux-64\",\n            \"  - click >=8\",\n            [\"  - osx-64\", \"  - osx-arm64\"],\n            \"  - linux-64\",\n        ),\n        (\n            \"\"\"\\\n            dependencies:\n              - pip: click ==0.1\n              - conda: click  # [linux64]\n            \"\"\",\n            \"osx-arm64\",\n            \"    - click ==0.1\",\n            [\"  - linux-64\"],\n            \"  - osx-arm64\",\n        ),\n    ],\n)\ndef test_merge_uses_selector_platforms_even_for_losing_alternatives(\n    tmp_path: Path,\n    content: str,\n    current_platform: str,\n    expected_dependency: str,\n    expected_platforms: list[str],\n    excluded_platform: str,\n) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(textwrap.dedent(content))\n    output_file = tmp_path / \"environment.yaml\"\n\n    with patch(\"unidep.utils.identify_current_platform\", return_value=current_platform):\n        _merge_command(\n            depth=1,\n            directory=tmp_path,\n            files=[req_file],\n            name=\"myenv\",\n            output=output_file,\n            stdout=False,\n            selector=\"comment\",\n            platforms=[],\n            optional_dependencies=[],\n            all_optional_dependencies=False,\n            ignore_pins=[],\n            skip_dependencies=[],\n            overwrite_pins=[],\n            verbose=False,\n        )\n\n    merged = output_file.read_text()\n    assert expected_dependency in merged\n    assert \"platforms:\" in merged\n    for expected_platform in expected_platforms:\n        assert expected_platform in merged\n    assert excluded_platform not in merged\n\n\ndef test_merge_command_includes_selected_optional_dependencies(\n    tmp_path: Path,\n) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n              - numpy\n            optional_dependencies:\n              docs:\n                - sphinx\n              test:\n                - pytest\n            \"\"\",\n        ),\n    )\n    output_file = tmp_path / \"environment.yaml\"\n\n    _merge_command(\n        depth=1,\n        directory=tmp_path,\n        files=[req_file],\n        name=\"myenv\",\n        output=output_file,\n        stdout=False,\n        selector=\"comment\",\n        platforms=[],\n        optional_dependencies=[\"docs\", \"test\"],\n        all_optional_dependencies=False,\n        ignore_pins=[],\n        skip_dependencies=[],\n        overwrite_pins=[],\n        verbose=False,\n    )\n\n    merged = output_file.read_text()\n    assert \"  - numpy\" in merged\n    assert \"  - sphinx\" in merged\n    assert \"  - pytest\" in merged\n\n\ndef test_merge_command_includes_all_optional_dependencies(\n    tmp_path: Path,\n) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n              - numpy\n            optional_dependencies:\n              docs:\n                - sphinx\n              test:\n                - pytest\n            \"\"\",\n        ),\n    )\n    output_file = tmp_path / \"environment.yaml\"\n\n    _merge_command(\n        depth=1,\n        directory=tmp_path,\n        files=[req_file],\n        name=\"myenv\",\n        output=output_file,\n        stdout=False,\n        selector=\"comment\",\n        platforms=[],\n        optional_dependencies=[],\n        all_optional_dependencies=True,\n        ignore_pins=[],\n        skip_dependencies=[],\n        overwrite_pins=[],\n        verbose=False,\n    )\n\n    merged = output_file.read_text()\n    assert \"  - numpy\" in merged\n    assert \"  - sphinx\" in merged\n    assert \"  - pytest\" in merged\n\n\ndef test_merge_command_includes_local_only_optional_dependencies(\n    tmp_path: Path,\n) -> None:\n    local_project = tmp_path / \"local-project\"\n    local_project.mkdir()\n    (local_project / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n              - adaptive\n            optional_dependencies:\n              test:\n                - pytest\n            \"\"\",\n        ),\n    )\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n              - numpy\n            optional_dependencies:\n              local:\n                - ./local-project[test]\n            \"\"\",\n        ),\n    )\n    output_file = tmp_path / \"environment.yaml\"\n\n    _merge_command(\n        depth=1,\n        directory=tmp_path,\n        files=[req_file],\n        name=\"myenv\",\n        output=output_file,\n        stdout=False,\n        selector=\"comment\",\n        platforms=[],\n        optional_dependencies=[\"local\"],\n        all_optional_dependencies=False,\n        ignore_pins=[],\n        skip_dependencies=[],\n        overwrite_pins=[],\n        verbose=False,\n    )\n\n    merged = output_file.read_text()\n    assert \"  - numpy\" in merged\n    assert \"  - adaptive\" in merged\n    assert \"  - pytest\" in merged\n\n\ndef test_merge_optional_dependency_extras_rejects_unknown_group(\n    tmp_path: Path,\n    capsys: pytest.CaptureFixture[str],\n) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            optional_dependencies:\n              docs:\n                - sphinx\n              test:\n                - pytest\n            \"\"\",\n        ),\n    )\n\n    with pytest.raises(SystemExit, match=\"1\"):\n        _merge_optional_dependency_extras(\n            found_files=[req_file],\n            optional_dependencies=[\"dev\"],\n            all_optional_dependencies=False,\n        )\n\n    captured = capsys.readouterr()\n    assert \"Unknown optional dependency group(s): `dev`\" in captured.out\n    assert \"Valid groups: `docs`, `test`.\" in captured.out\n\n\ndef test_merge_optional_dependency_extras_validates_across_all_files(\n    tmp_path: Path,\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project1.mkdir()\n    req1 = project1 / \"pyproject.toml\"\n    req1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            [tool.unidep]\n            [tool.unidep.optional_dependencies]\n            docs = [\"sphinx\"]\n            \"\"\",\n        ),\n    )\n    project2 = tmp_path / \"project2\"\n    project2.mkdir()\n    req2 = project2 / \"requirements.yaml\"\n    req2.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            optional_dependencies:\n              test:\n                - pytest\n            \"\"\",\n        ),\n    )\n\n    extras = _merge_optional_dependency_extras(\n        found_files=[req1, req2],\n        optional_dependencies=[\"test\"],\n        all_optional_dependencies=False,\n    )\n\n    assert extras == [[\"test\"], [\"test\"]]\n\n\ndef test_collect_available_optional_dependency_groups_preserves_local_only_groups(\n    tmp_path: Path,\n) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            optional_dependencies:\n              docs:\n                - sphinx\n              local:\n                - ../missing-project[test]\n            \"\"\",\n        ),\n    )\n\n    groups = _collect_available_optional_dependency_groups([req_file])\n\n    assert groups == [\"docs\", \"local\"]\n\n\ndef test_merge_optional_dependency_extras_reports_when_no_groups_exist(\n    tmp_path: Path,\n    capsys: pytest.CaptureFixture[str],\n) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\"dependencies:\\n  - numpy\\n\")\n\n    with pytest.raises(SystemExit, match=\"1\"):\n        _merge_optional_dependency_extras(\n            found_files=[req_file],\n            optional_dependencies=[\"dev\"],\n            all_optional_dependencies=False,\n        )\n\n    captured = capsys.readouterr()\n    assert \"Unknown optional dependency group(s): `dev`\" in captured.out\n    assert \"No optional dependency groups were found.\" in captured.out\n\n\ndef test_flatten_selected_dependency_entries_includes_optional_groups(\n    tmp_path: Path,\n) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n              - numpy\n            optional_dependencies:\n              dev:\n                - pytest\n            \"\"\",\n        ),\n    )\n\n    requirements = parse_requirements(req_file, extras=[[\"*\"]])\n    entries = _flatten_selected_dependency_entries(\n        requirements.dependency_entries,\n        requirements.optional_dependency_entries,\n    )\n\n    def entry_name(entry: Any) -> str:\n        conda = entry.conda\n        pip = entry.pip\n        if conda is not None:\n            return conda.name\n        assert pip is not None\n        return pip.name\n\n    assert [entry_name(entry) for entry in entries] == [\n        \"numpy\",\n        \"pytest\",\n    ]\n\n\ndef test_collect_selected_conda_like_platforms_uses_both_source_selectors(\n    tmp_path: Path,\n) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n              - conda: click  # [linux64]\n                pip: click  # [osx]\n            \"\"\",\n        ),\n    )\n\n    requirements = parse_requirements(req_file)\n    entries = _flatten_selected_dependency_entries(\n        requirements.dependency_entries,\n        requirements.optional_dependency_entries,\n    )\n\n    assert _collect_selected_conda_like_platforms(entries) == [\n        \"linux-64\",\n        \"osx-64\",\n        \"osx-arm64\",\n    ]\n\n\ndef test_collect_selected_conda_like_platforms_preserves_selector_platforms(\n    tmp_path: Path,\n) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n              - conda: click >=8\n              - pip: click  # [osx]\n            \"\"\",\n        ),\n    )\n\n    requirements = parse_requirements(req_file)\n    entries = _flatten_selected_dependency_entries(\n        requirements.dependency_entries,\n        requirements.optional_dependency_entries,\n    )\n\n    assert _collect_selected_conda_like_platforms(entries) == [\n        \"osx-64\",\n        \"osx-arm64\",\n    ]\n\n\ndef test_unidep_pixi_cli_optional_monorepo_env_includes_base(\n    tmp_path: Path,\n) -> None:\n    project1_dir = tmp_path / \"project1\"\n    project1_dir.mkdir()\n    req1 = project1_dir / \"requirements.yaml\"\n    req1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            channels:\n              - conda-forge\n            dependencies:\n              - numpy\n            optional_dependencies:\n              dev:\n                - pytest\n            platforms:\n              - linux-64\n            \"\"\",\n        ),\n    )\n\n    project2_dir = tmp_path / \"project2\"\n    project2_dir.mkdir()\n    req2 = project2_dir / \"requirements.yaml\"\n    req2.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            channels:\n              - conda-forge\n            dependencies:\n              - pandas\n            platforms:\n              - linux-64\n            \"\"\",\n        ),\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    result = subprocess.run(\n        [  # noqa: S607\n            \"unidep\",\n            \"pixi\",\n            \"--file\",\n            str(project1_dir),\n            \"--file\",\n            str(project2_dir),\n            \"--output\",\n            str(output_file),\n        ],\n        check=True,\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n    )\n\n    assert result.returncode == 0, \"Command failed to execute successfully\"\n    with output_file.open(\"rb\") as f:\n        data = tomllib.load(f)\n\n    envs = data[\"environments\"]\n    assert set(envs[\"project1-dev\"]) == {\"project1\", \"project1-dev\"}\n\n\ndef test_unidep_file_not_found_error() -> None:\n    # Path to the requirements file\n    requirements_path = REPO_ROOT / \"yolo\"\n\n    assert not requirements_path.exists()\n\n    # Run the unidep install command\n    result = subprocess.run(\n        [  # noqa: S607\n            \"unidep\",\n            \"conda\",\n            \"--file\",\n            str(requirements_path),\n        ],\n        check=False,\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n    )\n\n    assert result.returncode == 1, \"Command unexpectedly succeeded\"\n    assert \"❌ One or more files\" in result.stdout\n\n\ndef test_doubly_nested_project_folder_installable(\n    tmp_path: Path,\n) -> None:\n    example_folder = tmp_path / \"example\"\n    shutil.copytree(REPO_ROOT / \"example\", example_folder)\n\n    # Add an extra project\n    extra_projects = example_folder / \"extra_projects\"\n    extra_projects.mkdir(exist_ok=True, parents=True)\n    project4 = extra_projects / \"project4\"\n    project4.mkdir(exist_ok=True, parents=True)\n    (project4 / \"requirements.yaml\").write_text(\n        \"local_dependencies: [../../setup_py_project]\",\n    )\n    pyproject_toml = \"\\n\".join(  # noqa: FLY002\n        (\n            \"[build-system]\",\n            'requires = [\"setuptools\", \"unidep\"]',\n            'build-backend = \"setuptools.build_meta\"',\n        ),\n    )\n\n    (project4 / \"pyproject.toml\").write_text(pyproject_toml)\n    setup = \"\\n\".join(  # noqa: FLY002\n        (\n            \"from setuptools import setup\",\n            'setup(name=\"project4\", version=\"0.1.0\", description=\"yolo\", py_modules=[\"setup_py_project\"])',\n        ),\n    )\n    (project4 / \"setup.py\").write_text(setup)\n    (project4 / \"project4.py\").write_text(\"print('hello')\")\n\n    # Run the unidep install command\n    result = subprocess.run(\n        [  # noqa: S607\n            \"unidep\",\n            \"install\",\n            \"--dry-run\",\n            \"--editable\",\n            \"--no-dependencies\",\n            \"--no-uv\",\n            str(project4 / \"requirements.yaml\"),\n        ],\n        check=True,\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n    )\n\n    p1 = str(tmp_path / \"example\" / \"hatch_project\")\n    p2 = str(tmp_path / \"example\" / \"setup_py_project\")\n    p3 = str(tmp_path / \"example\" / \"setuptools_project\")\n    p4 = str(tmp_path / \"example\" / \"extra_projects\" / \"project4\")\n    pkgs = \" \".join([f\"-e {p}\" for p in sorted((p1, p2, p3, p4))])\n    assert f\"pip install --no-deps {pkgs}`\" in result.stdout\n\n    p5 = str(tmp_path / \"example\" / \"pyproject_toml_project\")\n    p6 = str(tmp_path / \"example\" / \"hatch2_project\")\n    # Test depth 2\n    result = subprocess.run(\n        [  # noqa: S607\n            \"unidep\",\n            \"install-all\",\n            \"--dry-run\",\n            \"--editable\",\n            \"--no-dependencies\",\n            \"--no-uv\",\n            \"--directory\",\n            str(example_folder),\n            \"--depth\",\n            \"2\",\n        ],\n        check=True,\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n    )\n    pkgs = \" \".join([f\"-e {p}\" for p in sorted((p1, p2, p3, p4, p5, p6))])\n    assert f\"pip install --no-deps {pkgs}`\" in result.stdout\n\n    # Test depth 1 (should not install project4)\n    result = subprocess.run(\n        [  # noqa: S607\n            \"unidep\",\n            \"install-all\",\n            \"--dry-run\",\n            \"--editable\",\n            \"--no-dependencies\",\n            \"--no-uv\",\n            \"--directory\",\n            str(example_folder),\n            \"--depth\",\n            \"1\",\n        ],\n        check=True,\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n    )\n    pkgs = \" \".join([f\"-e {p}\" for p in sorted((p1, p2, p3, p5, p6))])\n    assert f\"pip install --no-deps {pkgs}`\" in result.stdout\n\n\ndef test_pip_compile_command(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:\n    folder = tmp_path / \"example\"\n    shutil.copytree(REPO_ROOT / \"example\", folder)\n    with patch(\"subprocess.run\", return_value=None), patch(\n        \"importlib.util.find_spec\",\n        return_value=True,\n    ):\n        _pip_compile_command(\n            depth=2,\n            directory=folder,\n            platform=\"linux-64\",\n            ignore_pins=[],\n            skip_dependencies=[],\n            overwrite_pins=[],\n            verbose=True,\n            extra_flags=[\"--\", \"--allow-unsafe\"],\n        )\n    requirements_in = folder / \"requirements.in\"\n    assert requirements_in.exists()\n    with requirements_in.open() as f:\n        assert \"adaptive\" in f.read()\n    requirements_txt = folder / \"requirements.txt\"\n\n    assert (\n        f\"Locking dependencies with `pip-compile --output-file {requirements_txt} --allow-unsafe {requirements_in}`\"\n        in capsys.readouterr().out\n    )\n\n\ndef test_install_non_existing_file() -> None:\n    with pytest.raises(FileNotFoundError, match=r\"File `does_not_exist` not found\\.\"):\n        _install_command(\n            Path(\"does_not_exist\"),\n            conda_executable=\"\",  # type: ignore[arg-type]\n            conda_env_name=None,\n            conda_env_prefix=None,\n            conda_lock_file=None,\n            dry_run=True,\n            editable=True,\n            verbose=True,\n        )\n\n\ndef test_install_non_existing_folder(tmp_path: Path) -> None:\n    requirements_file = tmp_path / \"requirements.yaml\"\n    pyproject_file = tmp_path / \"pyproject.toml\"\n    match = re.escape(\n        f\"File `{requirements_file}` or `{pyproject_file}`\"\n        f\" (with unidep configuration) not found in `{tmp_path}`\",\n    )\n    with pytest.raises(FileNotFoundError, match=match):\n        _install_command(\n            tmp_path,\n            conda_executable=\"\",  # type: ignore[arg-type]\n            conda_env_name=None,\n            conda_env_prefix=None,\n            conda_lock_file=None,\n            dry_run=True,\n            editable=True,\n            verbose=True,\n        )\n\n\ndef test_version(capsys: pytest.CaptureFixture) -> None:\n    _print_versions()\n    captured = capsys.readouterr()\n    assert \"unidep location\" in captured.out\n    assert \"unidep version\" in captured.out\n    assert \"packaging\" in captured.out\n\n\ndef test_conda_env_list() -> None:\n    conda_executable = _identify_conda_executable()\n    _conda_env_list(conda_executable)\n\n\ndef test_conda_root_prefix_uses_conda_info_when_env_vars_are_unset(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    _conda_info.cache_clear()\n    monkeypatch.delenv(\"MAMBA_ROOT_PREFIX\", raising=False)\n    monkeypatch.delenv(\"CONDA_ROOT\", raising=False)\n    try:\n        with patch(\n            \"unidep._cli._conda_cli_command_json\",\n            return_value={\"root_prefix\": \"/opt/conda\", \"conda_prefix\": \"/fallback\"},\n        ) as conda_cli_command_json:\n            assert _conda_root_prefix(\"conda\") == Path(\"/opt/conda\")\n\n        conda_cli_command_json.assert_called_once_with(\"conda\", \"info\")\n    finally:\n        _conda_info.cache_clear()\n\n\n@pytest.mark.parametrize(\n    (\"which\", \"env_var\"),\n    [(\"conda\", \"CONDA_EXE\"), (\"micromamba\", \"MAMBA_EXE\")],\n)\ndef test_get_conda_executable_uses_env_var_fallback(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n    which: CondaExecutable,\n    env_var: str,\n) -> None:\n    exe = str(tmp_path / which)\n    monkeypatch.setenv(env_var, exe)\n    with patch(\"shutil.which\", return_value=None):\n        assert _get_conda_executable(which) == exe\n\n\ndef test_unidep_version_uses_rich_when_available(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n    capsys: pytest.CaptureFixture,\n) -> None:\n    rich_dir = tmp_path / \"rich\"\n    rich_dir.mkdir()\n    (rich_dir / \"__init__.py\").write_text(\"\")\n    (rich_dir / \"console.py\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            class Console:\n                def print(self, table):\n                    print(f\"RICH:{table.columns}|{table.rows}\")\n            \"\"\",\n        ),\n    )\n    (rich_dir / \"table.py\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            class Table:\n                def __init__(self, *, show_header):\n                    self.show_header = show_header\n                    self.columns = []\n                    self.rows = []\n\n                def add_column(self, name, *, style):\n                    self.columns.append((name, style))\n\n                def add_row(self, prop, value):\n                    self.rows.append((prop, value))\n            \"\"\",\n        ),\n    )\n\n    monkeypatch.syspath_prepend(str(tmp_path))\n    monkeypatch.delitem(sys.modules, \"rich\", raising=False)\n    monkeypatch.delitem(sys.modules, \"rich.console\", raising=False)\n    monkeypatch.delitem(sys.modules, \"rich.table\", raising=False)\n\n    _print_versions()\n\n    output = capsys.readouterr().out\n    assert \"RICH:[('Property', 'cyan'), ('Value', 'magenta')]\" in output\n    assert \"('unidep version',\" in output\n    assert \"('packaging version',\" in output\n\n\ndef test_pip_optional(tmp_path: Path) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - foo\n            optional_dependencies:\n                test:\n                    - bar\n            \"\"\",\n        ),\n    )\n    txt = _pip_subcommand(\n        file=[p],\n        platforms=[],\n        verbose=True,\n        ignore_pins=None,\n        skip_dependencies=None,\n        overwrite_pins=None,\n        separator=\" \",\n    )\n    assert txt == \"foo\"\n\n    txt = _pip_subcommand(\n        file=[f\"{p}[test]\"],  # type: ignore[list-item]\n        platforms=[],\n        verbose=True,\n        ignore_pins=None,\n        skip_dependencies=None,\n        overwrite_pins=None,\n        separator=\" \",\n    )\n    assert txt == \"foo bar\"\n\n\ndef test_capitalize_last_dir() -> None:\n    # Just needs to work for Windows paths\n    assert _capitalize_dir(r\"foo\\bar\\baz\") == r\"foo\\bar\\Baz\"\n    assert _capitalize_dir(r\"foo\\bar\\baz\", capitalize=False) == r\"foo\\bar\\baz\"\n    assert _capitalize_dir(r\"foo\\bar\\baz\", capitalize=True) == r\"foo\\bar\\Baz\"\n\n\n@pytest.mark.skipif(\n    os.name == \"nt\",\n    reason=\"Don't test on Windows to make sure that conda is not found.\",\n)\ndef test_find_conda_windows() -> None:\n    \"\"\"Tests whether the function searches the expected paths.\"\"\"\n    with pytest.raises(\n        FileNotFoundError,\n        match=r\"Could not find conda\\.\",\n    ) as excinfo:\n        _find_windows_path(\"conda\")\n    # This Windows hell... 🤦‍♂️\n    paths = [\n        r\"👉 %USERPROFILE%\\Anaconda3\\condabin\\conda.exe\",\n        r\"👉 %USERPROFILE%\\anaconda3\\condabin\\conda.exe\",\n        r\"👉 %USERPROFILE%\\Anaconda3\\condabin\\conda\",\n        r\"👉 %USERPROFILE%\\anaconda3\\condabin\\conda\",\n        r\"👉 %USERPROFILE%\\Anaconda3\\condabin\\conda.bat\",\n        r\"👉 %USERPROFILE%\\anaconda3\\condabin\\conda.bat\",\n        r\"👉 %USERPROFILE%\\Anaconda3\\Scripts\\conda.exe\",\n        r\"👉 %USERPROFILE%\\anaconda3\\Scripts\\conda.exe\",\n        r\"👉 %USERPROFILE%\\Anaconda3\\Scripts\\conda\",\n        r\"👉 %USERPROFILE%\\anaconda3\\Scripts\\conda\",\n        r\"👉 %USERPROFILE%\\Anaconda3\\Scripts\\conda.bat\",\n        r\"👉 %USERPROFILE%\\anaconda3\\Scripts\\conda.bat\",\n        r\"👉 %USERPROFILE%\\Anaconda3\\conda.exe\",\n        r\"👉 %USERPROFILE%\\anaconda3\\conda.exe\",\n        r\"👉 %USERPROFILE%\\Anaconda3\\conda\",\n        r\"👉 %USERPROFILE%\\anaconda3\\conda\",\n        r\"👉 %USERPROFILE%\\Anaconda3\\conda.bat\",\n        r\"👉 %USERPROFILE%\\anaconda3\\conda.bat\",\n        r\"👉 %USERPROFILE%\\Miniconda3\\condabin\\conda.exe\",\n        r\"👉 %USERPROFILE%\\miniconda3\\condabin\\conda.exe\",\n        r\"👉 %USERPROFILE%\\Miniconda3\\condabin\\conda\",\n        r\"👉 %USERPROFILE%\\miniconda3\\condabin\\conda\",\n        r\"👉 %USERPROFILE%\\Miniconda3\\condabin\\conda.bat\",\n        r\"👉 %USERPROFILE%\\miniconda3\\condabin\\conda.bat\",\n        r\"👉 %USERPROFILE%\\Miniconda3\\Scripts\\conda.exe\",\n        r\"👉 %USERPROFILE%\\miniconda3\\Scripts\\conda.exe\",\n        r\"👉 %USERPROFILE%\\Miniconda3\\Scripts\\conda\",\n        r\"👉 %USERPROFILE%\\miniconda3\\Scripts\\conda\",\n        r\"👉 %USERPROFILE%\\Miniconda3\\Scripts\\conda.bat\",\n        r\"👉 %USERPROFILE%\\miniconda3\\Scripts\\conda.bat\",\n        r\"👉 %USERPROFILE%\\Miniconda3\\conda.exe\",\n        r\"👉 %USERPROFILE%\\miniconda3\\conda.exe\",\n        r\"👉 %USERPROFILE%\\Miniconda3\\conda\",\n        r\"👉 %USERPROFILE%\\miniconda3\\conda\",\n        r\"👉 %USERPROFILE%\\Miniconda3\\conda.bat\",\n        r\"👉 %USERPROFILE%\\miniconda3\\conda.bat\",\n        r\"👉 C:\\Anaconda3\\condabin\\conda.exe\",\n        r\"👉 C:\\anaconda3\\condabin\\conda.exe\",\n        r\"👉 C:\\Anaconda3\\condabin\\conda\",\n        r\"👉 C:\\anaconda3\\condabin\\conda\",\n        r\"👉 C:\\Anaconda3\\condabin\\conda.bat\",\n        r\"👉 C:\\anaconda3\\condabin\\conda.bat\",\n        r\"👉 C:\\Anaconda3\\Scripts\\conda.exe\",\n        r\"👉 C:\\anaconda3\\Scripts\\conda.exe\",\n        r\"👉 C:\\Anaconda3\\Scripts\\conda\",\n        r\"👉 C:\\anaconda3\\Scripts\\conda\",\n        r\"👉 C:\\Anaconda3\\Scripts\\conda.bat\",\n        r\"👉 C:\\anaconda3\\Scripts\\conda.bat\",\n        r\"👉 C:\\Anaconda3\\conda.exe\",\n        r\"👉 C:\\anaconda3\\conda.exe\",\n        r\"👉 C:\\Anaconda3\\conda\",\n        r\"👉 C:\\anaconda3\\conda\",\n        r\"👉 C:\\Anaconda3\\conda.bat\",\n        r\"👉 C:\\anaconda3\\conda.bat\",\n        r\"👉 C:\\Miniconda3\\condabin\\conda.exe\",\n        r\"👉 C:\\miniconda3\\condabin\\conda.exe\",\n        r\"👉 C:\\Miniconda3\\condabin\\conda\",\n        r\"👉 C:\\miniconda3\\condabin\\conda\",\n        r\"👉 C:\\Miniconda3\\condabin\\conda.bat\",\n        r\"👉 C:\\miniconda3\\condabin\\conda.bat\",\n        r\"👉 C:\\Miniconda3\\Scripts\\conda.exe\",\n        r\"👉 C:\\miniconda3\\Scripts\\conda.exe\",\n        r\"👉 C:\\Miniconda3\\Scripts\\conda\",\n        r\"👉 C:\\miniconda3\\Scripts\\conda\",\n        r\"👉 C:\\Miniconda3\\Scripts\\conda.bat\",\n        r\"👉 C:\\miniconda3\\Scripts\\conda.bat\",\n        r\"👉 C:\\Miniconda3\\conda.exe\",\n        r\"👉 C:\\miniconda3\\conda.exe\",\n        r\"👉 C:\\Miniconda3\\conda\",\n        r\"👉 C:\\miniconda3\\conda\",\n        r\"👉 C:\\Miniconda3\\conda.bat\",\n        r\"👉 C:\\miniconda3\\conda.bat\",\n        r\"👉 C:\\ProgramData\\Anaconda3\\condabin\\conda.exe\",\n        r\"👉 C:\\ProgramData\\anaconda3\\condabin\\conda.exe\",\n        r\"👉 C:\\ProgramData\\Anaconda3\\condabin\\conda\",\n        r\"👉 C:\\ProgramData\\anaconda3\\condabin\\conda\",\n        r\"👉 C:\\ProgramData\\Anaconda3\\condabin\\conda.bat\",\n        r\"👉 C:\\ProgramData\\anaconda3\\condabin\\conda.bat\",\n        r\"👉 C:\\ProgramData\\Anaconda3\\Scripts\\conda.exe\",\n        r\"👉 C:\\ProgramData\\anaconda3\\Scripts\\conda.exe\",\n        r\"👉 C:\\ProgramData\\Anaconda3\\Scripts\\conda\",\n        r\"👉 C:\\ProgramData\\anaconda3\\Scripts\\conda\",\n        r\"👉 C:\\ProgramData\\Anaconda3\\Scripts\\conda.bat\",\n        r\"👉 C:\\ProgramData\\anaconda3\\Scripts\\conda.bat\",\n        r\"👉 C:\\ProgramData\\Anaconda3\\conda.exe\",\n        r\"👉 C:\\ProgramData\\anaconda3\\conda.exe\",\n        r\"👉 C:\\ProgramData\\Anaconda3\\conda\",\n        r\"👉 C:\\ProgramData\\anaconda3\\conda\",\n        r\"👉 C:\\ProgramData\\Anaconda3\\conda.bat\",\n        r\"👉 C:\\ProgramData\\anaconda3\\conda.bat\",\n        r\"👉 C:\\ProgramData\\Miniconda3\\condabin\\conda.exe\",\n        r\"👉 C:\\ProgramData\\miniconda3\\condabin\\conda.exe\",\n        r\"👉 C:\\ProgramData\\Miniconda3\\condabin\\conda\",\n        r\"👉 C:\\ProgramData\\miniconda3\\condabin\\conda\",\n        r\"👉 C:\\ProgramData\\Miniconda3\\condabin\\conda.bat\",\n        r\"👉 C:\\ProgramData\\miniconda3\\condabin\\conda.bat\",\n        r\"👉 C:\\ProgramData\\Miniconda3\\Scripts\\conda.exe\",\n        r\"👉 C:\\ProgramData\\miniconda3\\Scripts\\conda.exe\",\n        r\"👉 C:\\ProgramData\\Miniconda3\\Scripts\\conda\",\n        r\"👉 C:\\ProgramData\\miniconda3\\Scripts\\conda\",\n        r\"👉 C:\\ProgramData\\Miniconda3\\Scripts\\conda.bat\",\n        r\"👉 C:\\ProgramData\\miniconda3\\Scripts\\conda.bat\",\n        r\"👉 C:\\ProgramData\\Miniconda3\\conda.exe\",\n        r\"👉 C:\\ProgramData\\miniconda3\\conda.exe\",\n        r\"👉 C:\\ProgramData\\Miniconda3\\conda\",\n        r\"👉 C:\\ProgramData\\miniconda3\\conda\",\n        r\"👉 C:\\ProgramData\\Miniconda3\\conda.bat\",\n        r\"👉 C:\\ProgramData\\miniconda3\\conda.bat\",\n    ]\n    for path in paths:\n        assert path in excinfo.value.args[0]\n\n\ndef test_find_windows_path_returns_existing_mamba_location(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.setattr(\n        \"os.path.exists\",\n        lambda path: \"mambaforge\" in path and str(path).endswith(\"mamba.exe\"),\n    )\n    found = _find_windows_path(\"mamba\")\n    assert found.endswith(r\"mambaforge\\condabin\\mamba.exe\")\n\n\ndef test_find_windows_path_returns_existing_micromamba_location(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.setattr(\n        \"os.path.exists\",\n        lambda path: \"micromamba\" in path and str(path).endswith(\"micromamba.exe\"),\n    )\n    found = _find_windows_path(\"micromamba\")\n    assert found.endswith(r\"Micromamba\\condabin\\micromamba.exe\")\n\n\n@contextmanager\ndef set_env_var(key: str, value: str) -> Generator[None, None, None]:\n    original_value = os.environ.get(key)\n    os.environ[key] = value\n    try:\n        yield\n    finally:\n        if original_value is None:\n            del os.environ[key]\n        else:\n            os.environ[key] = original_value\n\n\n@pytest.mark.skipif(\n    os.name == \"nt\",\n    reason=\"On Windows it will search for Conda because of `_maybe_exe`.\",\n)\ndef test_maybe_conda_run() -> None:\n    with set_env_var(\"CONDA_EXE\", \"conda\"):\n        result = _maybe_conda_run(\"conda\", \"my_env\", None)\n        assert result == [\"conda\", \"run\", \"--name\", \"my_env\"]\n\n    p = Path(\"/path/to/env\")\n    with set_env_var(\"CONDA_EXE\", \"conda\"):\n        result = _maybe_conda_run(\"conda\", None, p)\n        assert result == [\"conda\", \"run\", \"--prefix\", str(p)]\n\n    with set_env_var(\"MAMBA_EXE\", \"mamba\"):\n        result = _maybe_conda_run(\"mamba\", \"my_env\", None)\n        assert result == [\"mamba\", \"run\", \"--name\", \"my_env\"]\n\n\ndef test_maybe_conda_run_without_executable_returns_empty() -> None:\n    assert _maybe_conda_run(None, \"my_env\", None) == []\n\n\ndef test_maybe_conda_run_without_active_environment_returns_empty(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.delenv(\"CONDA_PREFIX\", raising=False)\n    monkeypatch.delenv(\"MAMBA_ROOT_PREFIX\", raising=False)\n    assert _maybe_conda_run(\"conda\", None, None) == []\n\n\ndef test_maybe_create_conda_env_args_creates_env(\n    monkeypatch: pytest.MonkeyPatch,\n    capsys: pytest.CaptureFixture,\n) -> None:\n    \"\"\"Test that _maybe_create_conda_env_args creates the environment if it doesn't exist.\n\n    This simulates running:\n      unidep install --conda-env-name non-existing-env .\n    and checks that the function to create a conda environment is called.\n    \"\"\"\n    # Create a flag to record if _create_conda_environment is called\n    created = []\n\n    # Define a fake _create_conda_environment that records its call\n    def fake_create(\n        conda_executable: CondaExecutable,  # noqa: ARG001\n        *args: str,\n    ) -> None:\n        created.append(args)\n        print(\"Fake create called with\", args)\n\n    # Patch the _create_conda_environment function\n    monkeypatch.setattr(\n        \"unidep._cli._create_conda_environment\",\n        fake_create,\n    )\n\n    # Patch _conda_env_name_to_prefix to simulate that the environment is missing.\n    def fake_env_name_to_prefix(\n        conda_executable: CondaExecutable,  # noqa: ARG001\n        env_name: str,  # noqa: ARG001\n        *,\n        raise_if_not_found: bool = True,  # noqa: ARG001\n    ) -> Path | None:\n        # Simulate that for \"non-existing-env\" no environment exists.\n        return None\n\n    monkeypatch.setattr(\n        \"unidep._cli._conda_env_name_to_prefix\",\n        fake_env_name_to_prefix,\n    )\n\n    # Now call _maybe_create_conda_env_args with a non-existing environment name.\n\n    args = _maybe_create_conda_env_args(\"conda\", \"non-existing-env\", None)\n\n    # Check that our fake_create was called (i.e. the environment creation was triggered)\n    assert created, (\n        \"Expected environment creation to be triggered for non-existing env.\"\n    )\n    # Also, the returned arguments should be the standard ones for a named env.\n    assert args == [\"--name\", \"non-existing-env\"]\n\n    # Optionally, verify that our fake function printed the expected message.\n    output = capsys.readouterr().out\n    assert \"Fake create called with\" in output\n\n    # Now with a prefix\n    prefix = Path(\"/home/user/micromamba/envs/non-existing-env\")\n    args = _maybe_create_conda_env_args(\"conda\", None, prefix)\n\n    # Check that our fake_create was called (i.e. the environment creation was triggered)\n    assert created, (\n        \"Expected environment creation to be triggered for non-existing env.\"\n    )\n    # Also, the returned arguments should be the standard ones for a named env.\n    assert args == [\"--prefix\", str(prefix)]\n\n    # Optionally, verify that our fake function printed the expected message.\n    output = capsys.readouterr().out\n    assert \"Fake create called with\" in output\n\n\ndef test_install_command_with_conda_lock_skips_dependency_install(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n    capsys: pytest.CaptureFixture,\n) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            channels:\n              - conda-forge\n            dependencies:\n              - numpy\n            \"\"\",\n        ),\n    )\n    created: list[tuple[Path, str]] = []\n\n    def fake_create_env_from_lock(\n        conda_lock_file: Path,\n        conda_executable: str,\n        **_: object,\n    ) -> None:\n        created.append((conda_lock_file, conda_executable))\n\n    def fake_python_executable(*_args: object) -> str:\n        return \"python\"\n\n    monkeypatch.setattr(\"unidep._cli._create_env_from_lock\", fake_create_env_from_lock)\n    monkeypatch.setattr(\"unidep._cli.identify_current_platform\", lambda: \"linux-64\")\n    monkeypatch.setattr(\"unidep._cli._python_executable\", fake_python_executable)\n\n    _install_command(\n        req_file,\n        conda_executable=\"conda\",\n        conda_env_name=\"test-env\",\n        conda_env_prefix=None,\n        conda_lock_file=Path(\"conda-lock.yml\"),\n        dry_run=True,\n        editable=False,\n        skip_local=True,\n        verbose=False,\n    )\n\n    assert created == [(Path(\"conda-lock.yml\"), \"conda\")]\n    output = capsys.readouterr().out\n    assert \"Installing conda dependencies\" not in output\n    assert \"Installing pip dependencies\" not in output\n\n\ndef test_unidep_merge_cli_optional_dependencies(tmp_path: Path) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n              - numpy\n            optional_dependencies:\n              docs:\n                - sphinx\n            \"\"\",\n        ),\n    )\n    output_file = tmp_path / \"environment.yaml\"\n\n    env = os.environ.copy()\n    env[\"PYTHONPATH\"] = str(REPO_ROOT)\n    result = subprocess.run(\n        [\n            sys.executable,\n            \"-c\",\n            \"from unidep._cli import main; main()\",\n            \"merge\",\n            \"--directory\",\n            str(tmp_path),\n            \"--depth\",\n            \"0\",\n            \"--output\",\n            str(output_file),\n            \"--optional-dependencies\",\n            \"docs\",\n        ],\n        check=True,\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n        env=env,\n    )\n\n    assert result.returncode == 0\n    merged = output_file.read_text()\n    assert \"  - numpy\" in merged\n    assert \"  - sphinx\" in merged\n\n\ndef test_unidep_merge_cli_all_optional_dependencies(tmp_path: Path) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n              - numpy\n            optional_dependencies:\n              docs:\n                - sphinx\n              test:\n                - pytest\n            \"\"\",\n        ),\n    )\n    output_file = tmp_path / \"environment.yaml\"\n\n    env = os.environ.copy()\n    env[\"PYTHONPATH\"] = str(REPO_ROOT)\n    result = subprocess.run(\n        [\n            sys.executable,\n            \"-c\",\n            \"from unidep._cli import main; main()\",\n            \"merge\",\n            \"--directory\",\n            str(tmp_path),\n            \"--depth\",\n            \"0\",\n            \"--output\",\n            str(output_file),\n            \"--all-optional-dependencies\",\n        ],\n        check=True,\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n        env=env,\n    )\n\n    assert result.returncode == 0\n    merged = output_file.read_text()\n    assert \"  - numpy\" in merged\n    assert \"  - sphinx\" in merged\n    assert \"  - pytest\" in merged\n\n\ndef test_unidep_merge_cli_rejects_unknown_optional_dependency_group(\n    tmp_path: Path,\n) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            optional_dependencies:\n              docs:\n                - sphinx\n            \"\"\",\n        ),\n    )\n\n    env = os.environ.copy()\n    env[\"PYTHONPATH\"] = str(REPO_ROOT)\n    result = subprocess.run(\n        [\n            sys.executable,\n            \"-c\",\n            \"from unidep._cli import main; main()\",\n            \"merge\",\n            \"--directory\",\n            str(tmp_path),\n            \"--depth\",\n            \"0\",\n            \"--optional-dependencies\",\n            \"dev\",\n        ],\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n        env=env,\n        check=False,\n    )\n\n    assert result.returncode == 1\n    assert \"Unknown optional dependency group(s): `dev`\" in result.stdout\n    assert \"Valid groups: `docs`.\" in result.stdout\n\n\ndef test_unidep_merge_cli_rejects_mutually_exclusive_optional_flags(\n    tmp_path: Path,\n) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            optional_dependencies:\n              docs:\n                - sphinx\n            \"\"\",\n        ),\n    )\n\n    env = os.environ.copy()\n    env[\"PYTHONPATH\"] = str(REPO_ROOT)\n    result = subprocess.run(\n        [\n            sys.executable,\n            \"-c\",\n            \"from unidep._cli import main; main()\",\n            \"merge\",\n            \"--directory\",\n            str(tmp_path),\n            \"--depth\",\n            \"0\",\n            \"--optional-dependencies\",\n            \"docs\",\n            \"--all-optional-dependencies\",\n        ],\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n        env=env,\n        check=False,\n    )\n\n    assert result.returncode == 2\n    assert \"not allowed with argument\" in result.stderr\n\n\ndef test_unidep_merge_cli_optional_dependencies_across_multiple_files(\n    tmp_path: Path,\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project1.mkdir()\n    (project1 / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n              - numpy\n            optional_dependencies:\n              docs:\n                - sphinx\n            \"\"\",\n        ),\n    )\n\n    project2 = tmp_path / \"project2\"\n    project2.mkdir()\n    (project2 / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n              - pandas\n            optional_dependencies:\n              test:\n                - pytest\n            \"\"\",\n        ),\n    )\n\n    output_file = tmp_path / \"environment.yaml\"\n    env = os.environ.copy()\n    env[\"PYTHONPATH\"] = str(REPO_ROOT)\n    result = subprocess.run(\n        [\n            sys.executable,\n            \"-c\",\n            \"from unidep._cli import main; main()\",\n            \"merge\",\n            \"--directory\",\n            str(tmp_path),\n            \"--depth\",\n            \"1\",\n            \"--output\",\n            str(output_file),\n            \"--optional-dependencies\",\n            \"test\",\n        ],\n        check=True,\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n        env=env,\n    )\n\n    assert result.returncode == 0\n    merged = output_file.read_text()\n    assert \"  - numpy\" in merged\n    assert \"  - pandas\" in merged\n    assert \"  - pytest\" in merged\n    assert \"  - sphinx\" not in merged\n"
  },
  {
    "path": "tests/test_cli_install_conda_lock.py",
    "content": "\"\"\"Tests for the `unidep._cli` module (installing conda environment from lock file).\"\"\"\n\nimport subprocess\nfrom pathlib import Path\nfrom unittest.mock import Mock, call, patch\n\nimport pytest\n\nfrom unidep._cli import (\n    CondaExecutable,\n    _create_env_from_lock,\n    _verify_conda_lock_installed,\n)\n\n\n@pytest.fixture\ndef mock_subprocess_run(monkeypatch: pytest.MonkeyPatch) -> Mock:\n    mock = Mock()\n    monkeypatch.setattr(\"subprocess.run\", mock)\n    return mock\n\n\n@pytest.fixture\ndef mock_print(monkeypatch: pytest.MonkeyPatch) -> Mock:\n    mock = Mock()\n    monkeypatch.setattr(\"builtins.print\", mock)\n    return mock\n\n\n@pytest.mark.parametrize(\"conda_executable\", [\"conda\", \"mamba\", \"micromamba\"])\n@pytest.mark.parametrize(\n    \"env_spec\",\n    [\n        {\"conda_env_name\": \"test_env\", \"conda_env_prefix\": None},\n        {\"conda_env_name\": None, \"conda_env_prefix\": Path(\"/path/to/env\")},\n    ],\n)\ndef test_create_env_from_lock_dry_run(\n    conda_executable: CondaExecutable,\n    env_spec: dict,\n    mock_subprocess_run: Mock,\n    mock_print: Mock,\n) -> None:\n    conda_lock_file = Path(\"conda-lock.yml\")\n\n    with patch(\"unidep._cli._verify_conda_lock_installed\"):\n        _create_env_from_lock(\n            conda_lock_file=conda_lock_file,\n            conda_executable=conda_executable,\n            **env_spec,\n            dry_run=True,\n            verbose=True,\n        )\n\n    # Check that subprocess.run was not called\n    mock_subprocess_run.assert_not_called()\n\n    # Check that appropriate messages were printed\n    env_identifier = (\n        f\"'{env_spec['conda_env_name']}'\"\n        if env_spec[\"conda_env_name\"]\n        else f\"at '{env_spec['conda_env_prefix']}'\"\n    )\n\n    assert len(mock_print.call_args_list) == 2\n\n    # Check the first message (creating environment)\n    first_call = mock_print.call_args_list[0]\n    assert first_call.args[0].startswith(\n        f\"📦 Creating conda environment {env_identifier} with \",\n    )\n\n    # Check the command string separately\n    cmd_str = first_call.args[0]\n    if conda_executable == \"micromamba\":\n        assert \"micromamba create\" in cmd_str or \"micromamba.exe create\" in cmd_str\n        assert \"-f conda-lock.yml\" in cmd_str\n        assert \"--yes\" in cmd_str\n        assert \"--verbose\" in cmd_str\n    else:\n        assert \"conda-lock install\" in cmd_str\n        assert \"--log-level=DEBUG\" in cmd_str\n        if conda_executable == \"mamba\":\n            assert \"--mamba\" in cmd_str\n        elif conda_executable == \"conda\":\n            assert \"--conda conda\" in cmd_str\n\n    if env_spec[\"conda_env_name\"]:\n        assert f\"--name {env_spec['conda_env_name']}\" in cmd_str\n    elif env_spec[\"conda_env_prefix\"]:\n        assert f\"--prefix {env_spec['conda_env_prefix']}\" in cmd_str\n\n    # Check the second message (dry run completed)\n    assert mock_print.call_args_list[1] == call(\n        \"🏁 Dry run completed. No environment was created.\",\n    )\n\n\ndef test_create_env_from_lock_no_env_specified(mock_print: Mock) -> None:\n    conda_lock_file = Path(\"conda-lock.yml\")\n\n    with pytest.raises(SystemExit):\n        _create_env_from_lock(\n            conda_lock_file=conda_lock_file,\n            conda_executable=\"conda\",\n            conda_env_name=None,\n            conda_env_prefix=None,\n            dry_run=True,\n            verbose=True,\n        )\n\n    mock_print.assert_called_once_with(\n        \"❌ Please provide either `--conda-env-name` or\"\n        \" `--conda-env-prefix` when using `--conda-lock-file`.\",\n    )\n\n\ndef test_create_env_from_lock_verifies_installation_for_conda(\n    mock_subprocess_run: Mock,\n) -> None:\n    with patch(\"unidep._cli._verify_conda_lock_installed\") as verify:\n        _create_env_from_lock(\n            conda_lock_file=Path(\"conda-lock.yml\"),\n            conda_executable=\"conda\",\n            conda_env_name=\"test-env\",\n            conda_env_prefix=None,\n            dry_run=False,\n            verbose=False,\n        )\n\n    verify.assert_called_once()\n    mock_subprocess_run.assert_called_once()\n\n\ndef test_verify_conda_lock_installed_not_found(\n    monkeypatch: pytest.MonkeyPatch,\n    mock_print: Mock,\n) -> None:\n    monkeypatch.setattr(\"shutil.which\", lambda _: None)\n\n    with pytest.raises(SystemExit):\n        _verify_conda_lock_installed()\n\n    assert (\n        \"❌ conda-lock is not installed or not found in PATH.\"\n        in mock_print.call_args[0][0]\n    )\n\n\ndef test_verify_conda_lock_installed_not_working(\n    monkeypatch: pytest.MonkeyPatch,\n    mock_print: Mock,\n) -> None:\n    monkeypatch.setattr(\"shutil.which\", lambda _: \"/path/to/conda-lock\")\n    monkeypatch.setattr(\n        subprocess,\n        \"run\",\n        Mock(side_effect=subprocess.CalledProcessError(1, \"conda-lock\")),\n    )\n\n    with pytest.raises(SystemExit):\n        _verify_conda_lock_installed()\n\n    assert (\n        \"❌ conda-lock is installed but not working correctly.\"\n        in mock_print.call_args[0][0]\n    )\n"
  },
  {
    "path": "tests/test_conda_lock.py",
    "content": "\"\"\"unidep conda-lock tests.\"\"\"\n\nfrom __future__ import annotations\n\nimport shutil\nimport sys\nimport types\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any\nfrom unittest.mock import patch\n\nimport pytest\nfrom ruamel.yaml import YAML\n\nfrom unidep._conda_lock import (\n    LockSpec,\n    _check_consistent_lock_files,\n    _conda_lock_subpackage,\n    _conda_lock_subpackages,\n    _download_and_get_package_names,\n    _handle_missing_keys,\n    _parse_conda_lock_packages,\n    conda_lock_command,\n)\nfrom unidep.utils import remove_top_comments\n\nif TYPE_CHECKING:\n    from unidep.platform_definitions import CondaPip, Platform\n\n\ndef test_conda_lock_command(tmp_path: Path) -> None:\n    folder = tmp_path / \"simple_monorepo\"\n    shutil.copytree(Path(__file__).parent / \"simple_monorepo\", folder)\n    with patch(\"unidep._conda_lock._run_conda_lock\", return_value=None):\n        conda_lock_command(\n            depth=1,\n            directory=folder,\n            files=None,\n            platforms=[\"linux-64\", \"osx-arm64\"],\n            verbose=True,\n            only_global=False,\n            check_input_hash=True,\n            ignore_pins=[],\n            overwrite_pins=[],\n            skip_dependencies=[],\n            extra_flags=[\"--\", \"--micromamba\"],\n        )\n    with YAML(typ=\"safe\") as yaml:\n        with (folder / \"project1\" / \"conda-lock.yml\").open() as f:\n            lock1 = yaml.load(f)\n        with (folder / \"project2\" / \"conda-lock.yml\").open() as f:\n            lock2 = yaml.load(f)\n\n    assert [p[\"name\"] for p in lock1[\"package\"] if p[\"platform\"] == \"osx-arm64\"] == [\n        \"bzip2\",\n        \"python_abi\",\n        \"tzdata\",\n    ]\n    assert [p[\"name\"] for p in lock2[\"package\"] if p[\"platform\"] == \"osx-arm64\"] == [\n        \"python_abi\",\n        \"tzdata\",\n    ]\n\n\ndef test_conda_lock_command_pip_package_with_conda_dependency(tmp_path: Path) -> None:\n    folder = tmp_path / \"test-pip-package-with-conda-dependency\"\n    shutil.copytree(\n        Path(__file__).parent / \"test-pip-package-with-conda-dependency\",\n        folder,\n    )\n    with patch(\"unidep._conda_lock._run_conda_lock\", return_value=None):\n        conda_lock_command(\n            depth=1,\n            directory=folder,\n            files=None,\n            platforms=[\"linux-64\"],\n            verbose=True,\n            only_global=False,\n            check_input_hash=True,\n            ignore_pins=[],\n            overwrite_pins=[],\n            skip_dependencies=[],\n            extra_flags=[],\n        )\n    with YAML(typ=\"safe\") as yaml:\n        with (folder / \"project1\" / \"conda-lock.yml\").open() as f:\n            lock1 = yaml.load(f)\n        with (folder / \"project2\" / \"conda-lock.yml\").open() as f:\n            lock2 = yaml.load(f)\n    assert [p[\"name\"] for p in lock1[\"package\"]] == [\n        \"_libgcc_mutex\",\n        \"_openmp_mutex\",\n        \"bzip2\",\n        \"ca-certificates\",\n        \"ld_impl_linux-64\",\n        \"libexpat\",\n        \"libffi\",\n        \"libgcc-ng\",\n        \"libgomp\",\n        \"libnsl\",\n        \"libsqlite\",\n        \"libstdcxx-ng\",\n        \"libuuid\",\n        \"libzlib\",\n        \"ncurses\",\n        \"openssl\",\n        \"pybind11\",\n        \"pybind11-global\",\n        \"python\",\n        \"python_abi\",\n        \"readline\",\n        \"tk\",\n        \"tzdata\",\n        \"xz\",\n    ]\n    assert [p[\"name\"] for p in lock2[\"package\"]] == [\n        \"_libgcc_mutex\",\n        \"_openmp_mutex\",\n        \"bzip2\",\n        \"ca-certificates\",\n        \"ld_impl_linux-64\",\n        \"libexpat\",\n        \"libffi\",\n        \"libgcc-ng\",\n        \"libgomp\",\n        \"libnsl\",\n        \"libsqlite\",\n        \"libstdcxx-ng\",\n        \"libuuid\",\n        \"libzlib\",\n        \"ncurses\",\n        \"openssl\",\n        \"pybind11\",\n        \"pybind11-global\",\n        \"python\",\n        \"python_abi\",\n        \"readline\",\n        \"tk\",\n        \"tzdata\",\n        \"xz\",\n        \"cutde\",\n        \"mako\",\n        \"markupsafe\",\n        \"rsync-time-machine\",\n    ]\n\n\ndef test_conda_lock_global_infers_selector_platforms(tmp_path: Path) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        \"\"\"\\\nchannels:\n  - conda-forge\ndependencies:\n  - cuda-toolkit  # [linux64]\n\"\"\",\n    )\n    with patch(\"unidep._conda_lock._run_conda_lock\", return_value=None), patch(\n        \"unidep.utils.identify_current_platform\",\n        return_value=\"osx-arm64\",\n    ):\n        conda_lock_command(\n            depth=1,\n            directory=tmp_path,\n            files=[req_file],\n            platforms=[],\n            verbose=False,\n            only_global=True,\n            check_input_hash=False,\n            ignore_pins=[],\n            overwrite_pins=[],\n            skip_dependencies=[],\n            extra_flags=[],\n        )\n\n    tmp_env = tmp_path / \"tmp.environment.yaml\"\n    with YAML(typ=\"safe\") as yaml, tmp_env.open() as f:\n        data = yaml.load(f)\n    assert data[\"platforms\"] == [\"linux-64\"]\n\n\n@pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\ndef test_conda_lock_command_pip_and_conda_different_name(\n    tmp_path: Path,\n    capsys: pytest.CaptureFixture,\n) -> None:\n    folder = tmp_path / \"test-pip-and-conda-different-name\"\n    shutil.copytree(Path(__file__).parent / \"test-pip-and-conda-different-name\", folder)\n    files = [\n        folder / \"project1\" / \"requirements.yaml\",\n        folder / \"project2\" / \"requirements.yaml\",\n    ]\n    with patch(\"unidep._conda_lock._run_conda_lock\", return_value=None):\n        conda_lock_command(\n            depth=1,\n            directory=folder,  # ignored when using files\n            files=files,\n            platforms=[\"linux-64\"],\n            verbose=True,\n            only_global=False,\n            check_input_hash=True,\n            ignore_pins=[],\n            overwrite_pins=[],\n            skip_dependencies=[],\n            extra_flags=[],\n        )\n    assert \"Missing keys\" not in capsys.readouterr().out\n\n\ndef test_remove_top_comments(tmp_path: Path) -> None:\n    test_file = tmp_path / \"test_file.txt\"\n    test_file.write_text(\n        \"# Comment line 1\\n# Comment line 2\\nActual content line 1\\nActual content line 2\",\n    )\n\n    remove_top_comments(test_file)\n\n    with test_file.open(\"r\") as file:\n        content = file.read()\n\n    assert content == \"Actual content line 1\\nActual content line 2\"\n\n\ndef test_handle_missing_keys(capsys: pytest.CaptureFixture) -> None:\n    lock_spec = LockSpec(\n        packages={\n            (\"conda\", \"linux-64\", \"python-nonexistent\"): {\n                \"name\": \"python-nonexistent\",\n                \"manager\": \"conda\",\n                \"platform\": \"linux-64\",\n                \"dependencies\": [],\n                \"url\": \"https://example.com/nonexistent\",\n            },\n        },\n        dependencies={(\"conda\", \"linux-64\", \"nonexistent\"): set()},\n    )\n    # Here the package name on pip contains the conda package name, so we will download\n    # the conda package to verify that this is our package.\n\n    locked: list[dict[str, Any]] = []\n    locked_keys: set[tuple[CondaPip, Platform, str]] = {}  # type: ignore[assignment]\n    missing_keys: set[tuple[CondaPip, Platform, str]] = {\n        (\"pip\", \"linux-64\", \"nonexistent\"),\n    }\n    with patch(\n        \"unidep._conda_lock._download_and_get_package_names\",\n        return_value=None,\n    ) as mock:\n        _handle_missing_keys(\n            lock_spec=lock_spec,\n            locked_keys=locked_keys,\n            missing_keys=missing_keys,\n            locked=locked,\n        )\n        mock.assert_called_once()\n\n    assert f\"❌ Missing keys {missing_keys}\" in capsys.readouterr().out\n    assert (\"pip\", \"linux-64\", \"nonexistent\") in missing_keys\n\n\ndef test_handle_missing_keys_adds_matching_conda_package() -> None:\n    pkg = {\n        \"name\": \"msgpack-python\",\n        \"manager\": \"conda\",\n        \"platform\": \"linux-64\",\n        \"dependencies\": {},\n        \"url\": \"https://example.com/msgpack-python.conda\",\n    }\n    lock_spec = LockSpec(\n        packages={(\"conda\", \"linux-64\", \"msgpack-python\"): pkg},\n        dependencies={(\"conda\", \"linux-64\", \"msgpack-python\"): set()},\n    )\n    locked: list[dict[str, Any]] = []\n    locked_keys: set[tuple[CondaPip, Platform, str]] = set()\n    missing_keys: set[tuple[CondaPip, Platform, str]] = {\n        (\"pip\", \"linux-64\", \"msgpack\"),\n    }\n\n    with patch(\n        \"unidep._conda_lock._download_and_get_package_names\",\n        return_value=[\"msgpack\"],\n    ):\n        _handle_missing_keys(\n            lock_spec=lock_spec,\n            locked_keys=locked_keys,\n            missing_keys=missing_keys,\n            locked=locked,\n        )\n\n    assert missing_keys == set()\n    assert locked == [pkg]\n    assert (\"conda\", \"linux-64\", \"msgpack-python\") in locked_keys\n\n\ndef test_download_and_get_package_names_reads_site_packages(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    def fake_urlretrieve(_url: str, filename: str) -> None:\n        Path(filename).write_text(\"archive\")\n\n    def fake_extract(\n        _src: str,\n        *,\n        dest_dir: str,\n        components: str | None = None,\n    ) -> None:\n        del components\n        site_packages = Path(dest_dir) / \"site-packages\"\n        (site_packages / \"pkg\").mkdir(parents=True)\n        (site_packages / \"pkg.dist-info\").mkdir()\n        (site_packages / \"pkg.egg-info\").mkdir()\n\n    api_module = types.ModuleType(\"conda_package_handling.api\")\n    api_module.extract = fake_extract  # type: ignore[attr-defined]\n    package_module = types.ModuleType(\"conda_package_handling\")\n    package_module.api = api_module  # type: ignore[attr-defined]\n\n    monkeypatch.setitem(sys.modules, \"conda_package_handling\", package_module)\n    monkeypatch.setitem(sys.modules, \"conda_package_handling.api\", api_module)\n    monkeypatch.setattr(\"urllib.request.urlretrieve\", fake_urlretrieve)\n\n    names = _download_and_get_package_names(\n        {\n            \"name\": \"pkg\",\n            \"manager\": \"conda\",\n            \"platform\": \"linux-64\",\n            \"url\": \"https://example.com/pkg.conda\",\n        },\n    )\n    assert names == [\"pkg\"]\n\n\ndef test_download_and_get_package_names_returns_none_without_python_dirs(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    def fake_urlretrieve(_url: str, filename: str) -> None:\n        Path(filename).write_text(\"archive\")\n\n    def fake_extract(\n        _src: str,\n        *,\n        dest_dir: str,\n        components: str | None = None,\n    ) -> None:\n        del components\n        (Path(dest_dir) / \"lib\" / \"not-python\").mkdir(parents=True)\n\n    api_module = types.ModuleType(\"conda_package_handling.api\")\n    api_module.extract = fake_extract  # type: ignore[attr-defined]\n    package_module = types.ModuleType(\"conda_package_handling\")\n    package_module.api = api_module  # type: ignore[attr-defined]\n\n    monkeypatch.setitem(sys.modules, \"conda_package_handling\", package_module)\n    monkeypatch.setitem(sys.modules, \"conda_package_handling.api\", api_module)\n    monkeypatch.setattr(\"urllib.request.urlretrieve\", fake_urlretrieve)\n\n    names = _download_and_get_package_names(\n        {\n            \"name\": \"pkg\",\n            \"manager\": \"conda\",\n            \"platform\": \"linux-64\",\n            \"url\": \"https://example.com/pkg.conda\",\n        },\n    )\n    assert names is None\n\n\ndef test_download_and_get_package_names_returns_none_without_lib_or_site_packages(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    def fake_urlretrieve(_url: str, filename: str) -> None:\n        Path(filename).write_text(\"archive\")\n\n    def fake_extract(\n        _src: str,\n        *,\n        dest_dir: str,\n        components: str | None = None,\n    ) -> None:\n        del components\n        (Path(dest_dir) / \"share\").mkdir(parents=True)\n\n    api_module = types.ModuleType(\"conda_package_handling.api\")\n    api_module.extract = fake_extract  # type: ignore[attr-defined]\n    package_module = types.ModuleType(\"conda_package_handling\")\n    package_module.api = api_module  # type: ignore[attr-defined]\n\n    monkeypatch.setitem(sys.modules, \"conda_package_handling\", package_module)\n    monkeypatch.setitem(sys.modules, \"conda_package_handling.api\", api_module)\n    monkeypatch.setattr(\"urllib.request.urlretrieve\", fake_urlretrieve)\n\n    names = _download_and_get_package_names(\n        {\n            \"name\": \"pkg\",\n            \"manager\": \"conda\",\n            \"platform\": \"linux-64\",\n            \"url\": \"https://example.com/pkg.conda\",\n        },\n    )\n    assert names is None\n\n\ndef test_download_and_get_package_names_returns_none_without_site_packages(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    def fake_urlretrieve(_url: str, filename: str) -> None:\n        Path(filename).write_text(\"archive\")\n\n    def fake_extract(\n        _src: str,\n        *,\n        dest_dir: str,\n        components: str | None = None,\n    ) -> None:\n        del components\n        (Path(dest_dir) / \"lib\" / \"python3.12\").mkdir(parents=True)\n\n    api_module = types.ModuleType(\"conda_package_handling.api\")\n    api_module.extract = fake_extract  # type: ignore[attr-defined]\n    package_module = types.ModuleType(\"conda_package_handling\")\n    package_module.api = api_module  # type: ignore[attr-defined]\n\n    monkeypatch.setitem(sys.modules, \"conda_package_handling\", package_module)\n    monkeypatch.setitem(sys.modules, \"conda_package_handling.api\", api_module)\n    monkeypatch.setattr(\"urllib.request.urlretrieve\", fake_urlretrieve)\n\n    names = _download_and_get_package_names(\n        {\n            \"name\": \"pkg\",\n            \"manager\": \"conda\",\n            \"platform\": \"linux-64\",\n            \"url\": \"https://example.com/pkg.conda\",\n        },\n    )\n    assert names is None\n\n\ndef test_conda_lock_subpackages_skips_root_requirements(\n    tmp_path: Path,\n) -> None:\n    root_req = tmp_path / \"requirements.yaml\"\n    root_req.write_text(\"dependencies:\\n  - numpy\\n\")\n    subdir = tmp_path / \"project\"\n    subdir.mkdir()\n    sub_req = subdir / \"requirements.yaml\"\n    sub_req.write_text(\"dependencies:\\n  - pandas\\n\")\n\n    conda_lock_file = tmp_path / \"conda-lock.yml\"\n    yaml = YAML(typ=\"rt\")\n    with conda_lock_file.open(\"w\") as fp:\n        yaml.dump(\n            {\n                \"metadata\": {\n                    \"channels\": [{\"url\": \"conda-forge\"}],\n                    \"platforms\": [\"linux-64\"],\n                },\n                \"package\": [],\n            },\n            fp,\n        )\n\n    with patch(\n        \"unidep._conda_lock.find_requirements_files\",\n        return_value=[root_req, sub_req],\n    ), patch(\n        \"unidep._conda_lock._conda_lock_subpackage\",\n        return_value=subdir / \"conda-lock.yml\",\n    ) as mock:\n        lock_files = _conda_lock_subpackages(tmp_path, 1, conda_lock_file)\n\n    mock.assert_called_once()\n    assert mock.call_args.kwargs[\"file\"] == sub_req\n    assert lock_files == [subdir / \"conda-lock.yml\"]\n\n\ndef test_check_consistent_lock_files_reports_mismatches(tmp_path: Path) -> None:\n    global_lock = tmp_path / \"global.yml\"\n    sub_lock = tmp_path / \"sub.yml\"\n    lock_data = {\n        \"metadata\": {\"channels\": [], \"platforms\": [\"linux-64\"]},\n        \"package\": [\n            {\n                \"name\": \"numpy\",\n                \"platform\": \"linux-64\",\n                \"manager\": \"conda\",\n                \"version\": \"1.0\",\n            },\n        ],\n    }\n    sub_data = {\n        \"metadata\": {\"channels\": [], \"platforms\": [\"linux-64\"]},\n        \"package\": [\n            {\n                \"name\": \"numpy\",\n                \"platform\": \"linux-64\",\n                \"manager\": \"conda\",\n                \"version\": \"2.0\",\n            },\n        ],\n    }\n    yaml = YAML(typ=\"safe\")\n    with global_lock.open(\"w\") as fp:\n        yaml.dump(lock_data, fp)\n    with sub_lock.open(\"w\") as fp:\n        yaml.dump(sub_data, fp)\n\n    mismatches = _check_consistent_lock_files(global_lock, [sub_lock])\n    assert len(mismatches) == 1\n    assert mismatches[0].name == \"numpy\"\n    assert mismatches[0].version == \"2.0\"\n    assert mismatches[0].version_global == \"1.0\"\n\n\ndef test_conda_lock_subpackage_uses_selected_same_name_pip_winner(\n    tmp_path: Path,\n    capsys: pytest.CaptureFixture[str],\n) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        \"\"\"\\\n        dependencies:\n          - conda: foo\n          - pip: foo >1\n        \"\"\",\n    )\n    lock_spec = LockSpec(\n        packages={\n            (\"pip\", \"linux-64\", \"foo\"): {\n                \"name\": \"foo\",\n                \"manager\": \"pip\",\n                \"platform\": \"linux-64\",\n                \"version\": \"2.0\",\n                \"dependencies\": {},\n            },\n        },\n        dependencies={(\"pip\", \"linux-64\", \"foo\"): set()},\n    )\n\n    output = _conda_lock_subpackage(\n        file=req_file,\n        lock_spec=lock_spec,\n        channels=[\"conda-forge\"],\n        platforms=[\"linux-64\"],\n        yaml=YAML(typ=\"rt\"),\n    )\n\n    assert \"Missing keys\" not in capsys.readouterr().out\n    yaml = YAML(typ=\"safe\")\n    with output.open() as fp:\n        data = yaml.load(fp)\n    assert [(pkg[\"manager\"], pkg[\"name\"]) for pkg in data[\"package\"]] == [\n        (\"pip\", \"foo\"),\n    ]\n\n\ndef test_conda_lock_subpackage_uses_selected_paired_different_name_pip_winner(\n    tmp_path: Path,\n    capsys: pytest.CaptureFixture[str],\n) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        \"\"\"\\\n        dependencies:\n          - conda: python-graphviz\n            pip: graphviz >1\n        \"\"\",\n    )\n    lock_spec = LockSpec(\n        packages={\n            (\"pip\", \"linux-64\", \"graphviz\"): {\n                \"name\": \"graphviz\",\n                \"manager\": \"pip\",\n                \"platform\": \"linux-64\",\n                \"version\": \"2.0\",\n                \"dependencies\": {},\n            },\n        },\n        dependencies={(\"pip\", \"linux-64\", \"graphviz\"): set()},\n    )\n\n    output = _conda_lock_subpackage(\n        file=req_file,\n        lock_spec=lock_spec,\n        channels=[\"conda-forge\"],\n        platforms=[\"linux-64\"],\n        yaml=YAML(typ=\"rt\"),\n    )\n\n    assert \"Missing keys\" not in capsys.readouterr().out\n    yaml = YAML(typ=\"safe\")\n    with output.open() as fp:\n        data = yaml.load(fp)\n    assert [(pkg[\"manager\"], pkg[\"name\"]) for pkg in data[\"package\"]] == [\n        (\"pip\", \"graphviz\"),\n    ]\n\n\ndef test_conda_lock_subpackage_uses_selected_pip_winner_with_extras(\n    tmp_path: Path,\n    capsys: pytest.CaptureFixture[str],\n) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        \"\"\"\\\n        dependencies:\n          - conda: adaptive\n            pip: adaptive[notebook]\n        \"\"\",\n    )\n    lock_spec = LockSpec(\n        packages={\n            (\"pip\", \"linux-64\", \"adaptive\"): {\n                \"name\": \"adaptive\",\n                \"manager\": \"pip\",\n                \"platform\": \"linux-64\",\n                \"version\": \"1.0\",\n                \"dependencies\": {\"rich\": \"13.0\"},\n            },\n            (\"pip\", \"linux-64\", \"rich\"): {\n                \"name\": \"rich\",\n                \"manager\": \"pip\",\n                \"platform\": \"linux-64\",\n                \"version\": \"13.0\",\n                \"dependencies\": {},\n            },\n        },\n        dependencies={\n            (\"pip\", \"linux-64\", \"adaptive\"): {\"rich\"},\n            (\"pip\", \"linux-64\", \"rich\"): set(),\n        },\n    )\n\n    output = _conda_lock_subpackage(\n        file=req_file,\n        lock_spec=lock_spec,\n        channels=[\"conda-forge\"],\n        platforms=[\"linux-64\"],\n        yaml=YAML(typ=\"rt\"),\n    )\n\n    assert \"Missing keys\" not in capsys.readouterr().out\n    yaml = YAML(typ=\"safe\")\n    with output.open() as fp:\n        data = yaml.load(fp)\n    assert [(pkg[\"manager\"], pkg[\"name\"]) for pkg in data[\"package\"]] == [\n        (\"pip\", \"adaptive\"),\n        (\"pip\", \"rich\"),\n    ]\n\n\ndef test_circular_dependency() -> None:\n    \"\"\"Test that circular dependencies are handled correctly.\n\n    This test is based on the following requirements.yml file:\n\n    ```yaml\n    channels:\n        - conda-forge\n    dependencies:\n        - sphinx\n    platforms:\n        - linux-64\n    ```\n\n    The sphinx package has a circular dependency to itself, e.g., `sphinx` depends\n    on `sphinxcontrib-applehelp` which depends on `sphinx`.\n\n    Then we called `unidep conda-lock` on the above requirements.yml file. The\n    bit to reproduce the error is in the `package` list below.\n    \"\"\"\n    package = [\n        {\n            \"name\": \"sphinx\",\n            \"manager\": \"conda\",\n            \"platform\": \"linux-64\",\n            \"dependencies\": {\"sphinxcontrib-applehelp\": \"\"},\n        },\n        {\n            \"name\": \"sphinxcontrib-applehelp\",\n            \"version\": \"1.0.8\",\n            \"manager\": \"conda\",\n            \"platform\": \"linux-64\",\n            \"dependencies\": {\"sphinx\": \">=5\"},\n        },\n    ]\n    lock_spec = _parse_conda_lock_packages(package)\n    assert lock_spec.packages == {\n        (\"conda\", \"linux-64\", \"sphinx\"): {\n            \"name\": \"sphinx\",\n            \"manager\": \"conda\",\n            \"platform\": \"linux-64\",\n            \"dependencies\": {\"sphinxcontrib-applehelp\": \"\"},\n        },\n        (\"conda\", \"linux-64\", \"sphinxcontrib-applehelp\"): {\n            \"name\": \"sphinxcontrib-applehelp\",\n            \"version\": \"1.0.8\",\n            \"manager\": \"conda\",\n            \"platform\": \"linux-64\",\n            \"dependencies\": {\"sphinx\": \">=5\"},\n        },\n    }\n"
  },
  {
    "path": "tests/test_dependencies_parsing_internal.py",
    "content": "\"\"\"Focused tests for active internal dependency-parsing helpers.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    import pytest\n\nfrom unidep._dependencies_parsing import (\n    _is_empty_git_submodule,\n    _move_optional_dependencies_to_dependencies,\n    parse_requirements,\n)\nfrom unidep.utils import PathWithExtras\n\n\ndef test_move_optional_dependencies_star_promotes_all_groups(\n    capsys: pytest.CaptureFixture[str],\n) -> None:\n    data = {\n        \"dependencies\": [\"numpy\"],\n        \"optional_dependencies\": {\n            \"dev\": [\"pytest\"],\n            \"docs\": [\"sphinx\"],\n        },\n    }\n\n    _move_optional_dependencies_to_dependencies(\n        data,\n        PathWithExtras(Path(\"requirements.yaml\"), [\"*\"]),\n        verbose=True,\n    )\n\n    assert data[\"dependencies\"] == [\"numpy\", \"pytest\", \"sphinx\"]\n    assert \"optional_dependencies\" not in data\n    assert \"Moving all optional dependencies\" in capsys.readouterr().out\n\n\ndef test_parse_requirements_skips_empty_paired_dependency_after_filtering(\n    tmp_path: Path,\n) -> None:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(\n        \"\"\"\\\n        dependencies:\n          - conda: numpy\n            pip: numpy\n        \"\"\",\n    )\n\n    requirements = parse_requirements(req_file, skip_dependencies=[\"numpy\"])\n\n    assert requirements.requirements == {}\n    assert requirements.dependency_entries == []\n\n\ndef test_is_empty_git_submodule_false_for_non_directory(tmp_path: Path) -> None:\n    file_path = tmp_path / \"file.txt\"\n    file_path.write_text(\"not a directory\")\n    assert _is_empty_git_submodule(file_path) is False\n"
  },
  {
    "path": "tests/test_dependency_selection.py",
    "content": "\"\"\"Tests for user-shaped dependency selection behavior.\"\"\"\n\nfrom __future__ import annotations\n\nimport textwrap\nfrom pathlib import Path, PureWindowsPath\nfrom typing import TYPE_CHECKING, Tuple, cast\n\nimport pytest\n\nfrom unidep._conflicts import VersionConflictError\nfrom unidep._dependencies_parsing import DependencyOrigin, parse_requirements\nfrom unidep._dependency_selection import (\n    MergedSourceCandidate,\n    _joined_pinnings_are_safely_satisfiable,\n    _origin_to_text,\n    collapse_selected_universals,\n    select_conda_like_requirements,\n    select_pip_requirements,\n)\n\nif TYPE_CHECKING:\n    from unidep.platform_definitions import Platform\n\n\ndef _write_requirements(tmp_path: Path, content: str) -> Path:\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(textwrap.dedent(content))\n    return req_file\n\n\ndef _selected_summary(\n    selected: dict[Platform | None, list[MergedSourceCandidate]],\n) -> dict[Platform | None, list[tuple[str, str, str | None]]]:\n    return {\n        platform: [\n            (candidate.source, candidate.spec.name, candidate.spec.pin)\n            for candidate in candidates\n        ]\n        for platform, candidates in selected.items()\n    }\n\n\ndef test_origin_to_text_includes_optional_group_and_local_chain() -> None:\n    origin = DependencyOrigin(\n        source_file=Path(\"requirements.yaml\"),\n        dependency_index=3,\n        optional_group=\"dev\",\n        local_dependency_chain=(Path(\"libs/a\"), Path(\"libs/b\")),\n    )\n    assert _origin_to_text(origin) == (\n        \"requirements.yaml, item 3, group dev, via libs/a -> libs/b\"\n    )\n\n\ndef test_origin_to_text_normalizes_windows_style_local_chain() -> None:\n    origin = DependencyOrigin(\n        source_file=Path(\"requirements.yaml\"),\n        dependency_index=3,\n        optional_group=\"dev\",\n        local_dependency_chain=cast(\n            Tuple[Path, ...],\n            (\n                PureWindowsPath(\"libs\\\\a\"),\n                PureWindowsPath(\"libs\\\\b\"),\n            ),\n        ),\n    )\n    assert _origin_to_text(origin) == (\n        \"requirements.yaml, item 3, group dev, via libs/a -> libs/b\"\n    )\n\n\ndef test_joined_pinnings_are_safely_satisfiable_for_user_shaped_pin_strings() -> None:\n    assert _joined_pinnings_are_safely_satisfiable(\n        [\">=2\", \">=1\", \">2\", \"<=3\", \"<4\"],\n    )\n    assert _joined_pinnings_are_safely_satisfiable([\"==1\", \"~=1.0\"])\n    assert not _joined_pinnings_are_safely_satisfiable([\"==2.*\", \"<=1\"])\n    assert not _joined_pinnings_are_safely_satisfiable([\"==1.*\", \"<=1\", \"!=1\"])\n    assert not _joined_pinnings_are_safely_satisfiable([\"!=1.*\"])\n    assert not _joined_pinnings_are_safely_satisfiable([\"===1\"])\n    assert not _joined_pinnings_are_safely_satisfiable(\n        [\"@ git+https://example.com/example.git\"],\n    )\n\n\ndef test_select_conda_like_requirements_prefers_pinned_conda_over_unpinned_pip(\n    tmp_path: Path,\n) -> None:\n    req_file = _write_requirements(\n        tmp_path,\n        \"\"\"\\\n        platforms:\n          - linux-64\n        dependencies:\n          - conda: click >=8\n            pip: click\n        \"\"\",\n    )\n\n    requirements = parse_requirements(req_file)\n    selected = select_conda_like_requirements(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n\n    assert _selected_summary(selected) == {\n        \"linux-64\": [(\"conda\", \"click\", \">=8\")],\n    }\n\n\ndef test_select_conda_like_requirements_prefers_pip_extras_over_conda(\n    tmp_path: Path,\n) -> None:\n    req_file = _write_requirements(\n        tmp_path,\n        \"\"\"\\\n        platforms:\n          - linux-64\n        dependencies:\n          - conda: adaptive\n            pip: adaptive[notebook]\n        \"\"\",\n    )\n\n    requirements = parse_requirements(req_file)\n    selected = select_conda_like_requirements(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n\n    assert _selected_summary(selected) == {\n        \"linux-64\": [(\"pip\", \"adaptive[notebook]\", None)],\n    }\n\n\ndef test_select_conda_like_requirements_prefers_narrower_pinned_selector_scope(\n    tmp_path: Path,\n) -> None:\n    req_file = _write_requirements(\n        tmp_path,\n        \"\"\"\\\n        platforms:\n          - linux-64\n          - osx-64\n          - osx-arm64\n        dependencies:\n          - conda: click >=8\n          - pip: click >1  # [osx]\n        \"\"\",\n    )\n\n    requirements = parse_requirements(req_file)\n    selected = select_conda_like_requirements(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n\n    assert _selected_summary(selected) == {\n        \"linux-64\": [(\"conda\", \"click\", \">=8\")],\n        \"osx-64\": [(\"pip\", \"click\", \">1\")],\n        \"osx-arm64\": [(\"pip\", \"click\", \">1\")],\n    }\n\n\ndef test_select_conda_like_requirements_reports_final_collisions_with_origins(\n    tmp_path: Path,\n) -> None:\n    req_file = _write_requirements(\n        tmp_path,\n        \"\"\"\\\n        platforms:\n          - linux-64\n        dependencies:\n          - conda: foo\n          - conda: python-foo\n            pip: foo >1\n        \"\"\",\n    )\n\n    requirements = parse_requirements(req_file)\n    match = (\n        r\"(?s)Final Dependency Collision:\"\n        r\".*'foo' on platform 'linux-64'\"\n        r\".*conda: foo \\(\"\n        r\".*requirements\\.yaml, item 1\"\n        r\".*pip: foo >1 \\(\"\n        r\".*requirements\\.yaml, item 2\"\n    )\n    with pytest.raises(ValueError, match=match):\n        select_conda_like_requirements(\n            requirements.dependency_entries,\n            requirements.platforms,\n        )\n\n\ndef test_select_pip_requirements_merges_supported_wildcard_pinnings(\n    tmp_path: Path,\n) -> None:\n    req_file = _write_requirements(\n        tmp_path,\n        \"\"\"\\\n        platforms:\n          - linux-64\n        dependencies:\n          - conda: numpy\n          - pip: foo ==1.*\n          - pip: foo >=1.5\n        \"\"\",\n    )\n\n    requirements = parse_requirements(req_file)\n    selected = select_pip_requirements(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n\n    assert _selected_summary(selected) == {\n        \"linux-64\": [(\"pip\", \"foo\", \"==1.*,>=1.5\")],\n    }\n\n\ndef test_select_pip_requirements_merges_compatible_compatible_release_pinnings(\n    tmp_path: Path,\n) -> None:\n    req_file = _write_requirements(\n        tmp_path,\n        \"\"\"\\\n        platforms:\n          - linux-64\n        dependencies:\n          - pip: foo ~=1.4\n          - pip: foo <2\n        \"\"\",\n    )\n\n    requirements = parse_requirements(req_file)\n    selected = select_pip_requirements(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n\n    assert _selected_summary(selected) == {\n        \"linux-64\": [(\"pip\", \"foo\", \"~=1.4,<2\")],\n    }\n\n\ndef test_select_pip_requirements_rejects_unsafely_merged_wildcard_pinnings(\n    tmp_path: Path,\n) -> None:\n    req_file = _write_requirements(\n        tmp_path,\n        \"\"\"\\\n        platforms:\n          - linux-64\n        dependencies:\n          - pip: foo ==1.*\n          - pip: foo >2\n        \"\"\",\n    )\n\n    requirements = parse_requirements(req_file)\n    with pytest.raises(VersionConflictError, match=\"Invalid version pinning '==1.\"):\n        select_pip_requirements(\n            requirements.dependency_entries,\n            requirements.platforms,\n        )\n\n\ndef test_select_pip_requirements_rejects_multiple_exact_pinnings(\n    tmp_path: Path,\n) -> None:\n    req_file = _write_requirements(\n        tmp_path,\n        \"\"\"\\\n        platforms:\n          - linux-64\n        dependencies:\n          - pip: foo ==1\n          - pip: foo ==2\n        \"\"\",\n    )\n\n    requirements = parse_requirements(req_file)\n    with pytest.raises(\n        VersionConflictError,\n        match=\"Multiple exact version pinnings found: ==1, ==2 for `foo`\",\n    ):\n        select_pip_requirements(\n            requirements.dependency_entries,\n            requirements.platforms,\n        )\n\n\ndef test_collapse_selected_universals_collapses_user_declared_universal_dependencies(\n    tmp_path: Path,\n) -> None:\n    req_file = _write_requirements(\n        tmp_path,\n        \"\"\"\\\n        platforms:\n          - linux-64\n          - osx-arm64\n        dependencies:\n          - conda: numpy >=1\n        \"\"\",\n    )\n\n    requirements = parse_requirements(req_file)\n    selected = select_conda_like_requirements(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n    collapsed = collapse_selected_universals(selected, requirements.platforms)\n\n    assert _selected_summary(collapsed) == {\n        None: [(\"conda\", \"numpy\", \">=1\")],\n    }\n"
  },
  {
    "path": "tests/test_local_wheels_and_zip.py",
    "content": "\"\"\"Tests for parsing local dependencies from wheels and zips.\"\"\"\n\nimport textwrap\nfrom pathlib import Path\nfrom typing import Literal\n\nimport pytest\n\nfrom unidep import parse_local_dependencies, parse_requirements\n\nfrom .helpers import maybe_as_toml\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_local_wheel(tmp_path: Path, toml_or_yaml: Literal[\"toml\", \"yaml\"]) -> None:\n    project1 = tmp_path / \"project1\"\n    project1.mkdir(exist_ok=True, parents=True)\n    r1 = project1 / \"requirements.yaml\"\n    r1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../example.whl\n            \"\"\",\n        ),\n    )\n    r1 = maybe_as_toml(toml_or_yaml, r1)\n    r1 = maybe_as_toml(toml_or_yaml, r1)\n\n    local_dep = tmp_path / \"example.whl\"\n    local_dep.touch()  # Create a dummy .whl file\n\n    dependencies = parse_local_dependencies(\n        r1,\n        check_pip_installable=False,\n        verbose=True,\n    )\n    assert dependencies[project1.resolve()] == [local_dep.resolve()]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_local_zip(tmp_path: Path, toml_or_yaml: Literal[\"toml\", \"yaml\"]) -> None:\n    project1 = tmp_path / \"project1\"\n    project1.mkdir(exist_ok=True, parents=True)\n    r1 = project1 / \"requirements.yaml\"\n    r1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../example.zip\n            \"\"\",\n        ),\n    )\n    r1 = maybe_as_toml(toml_or_yaml, r1)\n\n    local_dep = tmp_path / \"example.zip\"\n    local_dep.touch()  # Create a dummy .zip file\n\n    dependencies = parse_local_dependencies(r1, check_pip_installable=False)\n    assert dependencies[project1.resolve()] == [local_dep.resolve()]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_local_wheel_and_folder(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project1.mkdir(exist_ok=True, parents=True)\n    project2 = tmp_path / \"project2\"\n    project2.mkdir(exist_ok=True, parents=True)\n    (project2 / \"setup.py\").touch()  # Make project2 pip installable\n    r1 = project1 / \"requirements.yaml\"\n    r1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../example.whl\n                - ../project2\n            \"\"\",\n        ),\n    )\n    r1 = maybe_as_toml(toml_or_yaml, r1)\n\n    local_dep = tmp_path / \"example.whl\"\n    local_dep.touch()  # Create a dummy .whl file\n    with pytest.warns(UserWarning, match=\"is not managed by unidep\"):\n        dependencies = parse_local_dependencies(r1, check_pip_installable=False)\n    assert dependencies[project1.resolve()] == [\n        local_dep.resolve(),\n        project2.resolve(),\n    ]\n\n    requirements = parse_requirements(r1, verbose=True)\n    assert requirements.requirements == {}\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_local_wheel_with_extras(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project1.mkdir(exist_ok=True, parents=True)\n    r1 = project1 / \"requirements.yaml\"\n    r1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../example.whl[extra1,extra2]\n            \"\"\",\n        ),\n    )\n    r1 = maybe_as_toml(toml_or_yaml, r1)\n\n    local_dep = tmp_path / \"example.whl\"\n    local_dep.touch()  # Create a dummy .whl file\n\n    dependencies = parse_local_dependencies(r1, check_pip_installable=False)\n    assert dependencies[project1.resolve()] == [local_dep.resolve()]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_local_wheel_in_dependencies(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project1.mkdir(exist_ok=True, parents=True)\n    r1 = project1 / \"requirements.yaml\"\n    r1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../example.whl\n            \"\"\",\n        ),\n    )\n    r1 = maybe_as_toml(toml_or_yaml, r1)\n\n    local_dep = tmp_path / \"example.whl\"\n    local_dep.touch()  # Create a dummy .whl file\n\n    dependencies = parse_local_dependencies(r1, check_pip_installable=False)\n    assert dependencies[project1.resolve()] == [local_dep.resolve()]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_nested_local_dependencies_with_wheel(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project2 = tmp_path / \"project2\"\n    project3 = tmp_path / \"project3\"\n    for project in [project1, project2, project3]:\n        project.mkdir(exist_ok=True, parents=True)\n        (project / \"setup.py\").touch()  # Make projects pip installable\n\n    wheel_dep = tmp_path / \"example.whl\"\n    wheel_dep.touch()  # Create a dummy .whl file\n\n    r1 = project1 / \"requirements.yaml\"\n    r2 = project2 / \"requirements.yaml\"\n    r3 = project3 / \"requirements.yaml\"\n\n    r1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../project2\n            \"\"\",\n        ),\n    )\n\n    r2.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../project3\n                - ../example.whl\n            \"\"\",\n        ),\n    )\n\n    r3.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - pytest\n            \"\"\",\n        ),\n    )\n\n    r1 = maybe_as_toml(toml_or_yaml, r1)\n    r2 = maybe_as_toml(toml_or_yaml, r2)\n    r3 = maybe_as_toml(toml_or_yaml, r3)\n\n    local_dependencies = parse_local_dependencies(r1, verbose=True)\n\n    assert local_dependencies == {\n        project1.resolve(): [\n            wheel_dep.resolve(),\n            project2.resolve(),\n            project3.resolve(),\n        ],\n    }\n"
  },
  {
    "path": "tests/test_parse_yaml_local_dependencies.py",
    "content": "\"\"\"unidep's YAML parsing of the `local_dependencies` list.\"\"\"\n\nfrom __future__ import annotations\n\nimport shutil\nimport textwrap\nfrom contextlib import nullcontext\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nimport pytest\nfrom ruamel.yaml import YAML\n\nfrom unidep import (\n    find_requirements_files,\n    parse_local_dependencies,\n    parse_requirements,\n)\nfrom unidep._conflicts import resolve_conflicts\n\nfrom .helpers import maybe_as_toml\n\nif TYPE_CHECKING:\n    import sys\n\n    if sys.version_info >= (3, 8):\n        from typing import Literal\n    else:  # pragma: no cover\n        from typing_extensions import Literal\n\n\nREPO_ROOT = Path(__file__).parent.parent\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_circular_local_dependencies(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project1.mkdir(exist_ok=True, parents=True)\n    project2 = tmp_path / \"project2\"\n    project2.mkdir(exist_ok=True, parents=True)\n\n    r1 = project1 / \"requirements.yaml\"\n    r2 = project2 / \"requirements.yaml\"\n    r1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - adaptive-scheduler\n            local_dependencies:\n                - ../project2\n                - ../project2  # duplicate include (shouldn't affect the result)\n            \"\"\",\n        ),\n    )\n    # Test with old `includes` name\n    r2.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - adaptive\n            includes:  # `local_dependencies` was called `includes` in <=0.41.0\n                - ../project1\n            \"\"\",\n        ),\n    )\n    r1 = maybe_as_toml(toml_or_yaml, r1)\n    # Only convert r1 to toml, not r2, because we want to test that\n    with pytest.warns(DeprecationWarning, match=\"is deprecated since 0.42.0\"):\n        requirements = parse_requirements(r1, r2, verbose=False)\n    # Both will be duplicated because of the circular dependency\n    # but `resolve_conflicts` will remove the duplicates\n    assert len(requirements.requirements[\"adaptive\"]) == 4\n    assert len(requirements.requirements[\"adaptive-scheduler\"]) == 2\n    resolved = resolve_conflicts(requirements.requirements, requirements.platforms)\n    assert len(resolved[\"adaptive\"]) == 1\n    assert len(resolved[\"adaptive\"][None]) == 2\n    assert len(resolved[\"adaptive-scheduler\"]) == 1\n    assert len(resolved[\"adaptive-scheduler\"][None]) == 2\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_parse_local_dependencies(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project1.mkdir(exist_ok=True, parents=True)\n    project2 = tmp_path / \"project2\"\n    project2.mkdir(exist_ok=True, parents=True)\n    r1 = project1 / \"requirements.yaml\"\n    r2 = project2 / \"requirements.yaml\"\n    r1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../project2\n                - ../project2  # duplicate include (shouldn't affect the result)\n            \"\"\",\n        ),\n    )\n    r2.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../project1\n            \"\"\",\n        ),\n    )\n    r2 = maybe_as_toml(toml_or_yaml, r2)\n    # Only convert r2 to toml, not r1, because we want to test that\n    local_dependencies = parse_local_dependencies(\n        r1,\n        r2,\n        verbose=False,\n        check_pip_installable=False,\n    )\n    expected_dependencies = {\n        project1.resolve(): [project2.resolve()],\n        project2.resolve(): [project1.resolve()],\n    }\n    assert local_dependencies == expected_dependencies\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_parse_local_dependencies_respects_use(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    project = tmp_path / \"project\"\n    project.mkdir(parents=True, exist_ok=True)\n    for name in [\"dep-local\", \"dep-skip\", \"dep-pypi\"]:\n        dep_dir = project / name\n        dep_dir.mkdir()\n        (dep_dir / \"setup.py\").write_text(\n            \"from setuptools import setup\\nsetup(name='dep', version='0.0.1')\\n\",\n        )\n        (dep_dir / \"requirements.yaml\").write_text(\"dependencies: []\\n\")\n\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\n            local_dependencies:\n              - local: ./dep-local\n              - local: ./dep-skip\n                use: skip\n              - local: ./dep-pypi\n                pypi: company-dep>=1.0\n                use: pypi\n            \"\"\",\n        ),\n    )\n\n    req_file = maybe_as_toml(toml_or_yaml, req_file)\n\n    local_dependencies = parse_local_dependencies(\n        req_file,\n        verbose=False,\n        check_pip_installable=False,\n    )\n\n    assert local_dependencies == {\n        project.resolve(): [\n            (project / \"dep-local\").resolve(),\n        ],\n    }\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_nested_local_dependencies(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project2 = tmp_path / \"project2\"\n    project3 = tmp_path / \"project3\"\n    project4 = tmp_path / \"project4\"\n    for project in [project1, project2, project3, project4]:\n        project.mkdir(exist_ok=True, parents=True)\n\n    p1 = project1 / \"requirements.yaml\"\n    p2 = project2 / \"requirements.yaml\"\n    p3 = project3 / \"requirements.yaml\"\n    p4 = project4 / \"requirements.yaml\"\n    p1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../project2\n            \"\"\",\n        ),\n    )\n    p2.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../project3\n            \"\"\",\n        ),\n    )\n    p3.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../project4\n            \"\"\",\n        ),\n    )\n    p4.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            \"\"\",\n        ),\n    )\n    p1 = maybe_as_toml(toml_or_yaml, p1)\n    p2 = maybe_as_toml(toml_or_yaml, p2)\n    p3 = maybe_as_toml(toml_or_yaml, p3)\n    p4 = maybe_as_toml(toml_or_yaml, p4)\n    local_dependencies = parse_local_dependencies(\n        p1,\n        p2,\n        p3,\n        verbose=False,\n        check_pip_installable=False,\n    )\n    expected_dependencies = {\n        project1.resolve(): [\n            project2.resolve(),\n            project3.resolve(),\n            project4.resolve(),\n        ],\n        project2.resolve(): [project3.resolve(), project4.resolve()],\n        project3.resolve(): [project4.resolve()],\n    }\n    assert local_dependencies == expected_dependencies\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_nonexistent_local_dependencies(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project1.mkdir(exist_ok=True, parents=True)\n    r1 = project1 / \"requirements.yaml\"\n    r1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../nonexistent_project\n            \"\"\",\n        ),\n    )\n    r1 = maybe_as_toml(toml_or_yaml, r1)\n    with pytest.raises(FileNotFoundError, match=r\"not found\\.\"):\n        parse_local_dependencies(r1, verbose=False, check_pip_installable=False)\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_no_local_dependencies(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project1.mkdir(exist_ok=True, parents=True)\n    r1 = project1 / \"requirements.yaml\"\n    r1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - pandas\n            \"\"\",\n        ),\n    )\n    r1 = maybe_as_toml(toml_or_yaml, r1)\n    local_dependencies = parse_local_dependencies(\n        r1,\n        verbose=False,\n        check_pip_installable=False,\n    )\n    assert local_dependencies == {}\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_mixed_real_and_placeholder_dependencies(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project1.mkdir(exist_ok=True, parents=True)\n    r1 = project1 / \"requirements.yaml\"\n    r1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - scipy\n            local_dependencies:\n                - ../project1  # Self include (circular dependency)\n            \"\"\",\n        ),\n    )\n    r1 = maybe_as_toml(toml_or_yaml, r1)\n    local_dependencies = parse_local_dependencies(\n        r1,\n        verbose=False,\n        check_pip_installable=False,\n    )\n    assert local_dependencies == {}\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_parse_local_dependencies_pip_installable(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    example_folder = tmp_path / \"example\"\n    shutil.copytree(REPO_ROOT / \"example\", example_folder)\n\n    # Add an extra project\n    extra_project = example_folder / \"extra_project\"\n    extra_project.mkdir(exist_ok=True, parents=True)\n    (extra_project / \"requirements.yaml\").write_text(\n        \"local_dependencies: [../setup_py_project]\",\n    )\n\n    # Add a line to project1 local_dependencies\n    setup_py_project_req = example_folder / \"setup_py_project\" / \"requirements.yaml\"\n    yaml = YAML(typ=\"safe\")\n    with setup_py_project_req.open(\"r\") as f:\n        requirements = yaml.load(f)\n    requirements[\"local_dependencies\"].append(\"../extra_project\")\n    with setup_py_project_req.open(\"w\") as f:\n        yaml.dump(requirements, f)\n\n    setup_py_project_req = maybe_as_toml(toml_or_yaml, setup_py_project_req)\n    found_files = find_requirements_files(example_folder)\n    assert len(found_files) == 6\n\n    # Add a common requirements file\n    common_requirements = example_folder / \"common-requirements.yaml\"\n    common_requirements.write_text(\"local_dependencies: [./setup_py_project]\")\n    common_requirements = maybe_as_toml(toml_or_yaml, common_requirements)\n    found_files.append(common_requirements)\n\n    local_dependencies = parse_local_dependencies(\n        *found_files,\n        check_pip_installable=True,\n        verbose=True,\n    )\n    assert local_dependencies\n    # extra_project is not `pip installable` so it should not be included in the values()\n    assert local_dependencies == {\n        example_folder / \"setup_py_project\": [\n            example_folder / \"hatch_project\",\n            example_folder / \"setuptools_project\",\n        ],\n        example_folder / \"setuptools_project\": [\n            example_folder / \"hatch_project\",\n        ],\n        example_folder / \"pyproject_toml_project\": [\n            example_folder / \"hatch_project\",\n        ],\n        example_folder / \"extra_project\": [\n            example_folder / \"hatch_project\",\n            example_folder / \"setup_py_project\",\n            example_folder / \"setuptools_project\",\n        ],\n        example_folder: [\n            example_folder / \"hatch_project\",\n            example_folder / \"setup_py_project\",\n            example_folder / \"setuptools_project\",\n        ],\n    }\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_parse_local_dependencies_pip_installable_with_non_installable_project(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    example_folder = tmp_path / \"example\"\n    shutil.copytree(REPO_ROOT / \"example\", example_folder)\n\n    # Add an extra project\n    extra_project = example_folder / \"extra_project\"\n    extra_project.mkdir(exist_ok=True, parents=True)\n    r_extra = extra_project / \"requirements.yaml\"\n    r_extra.write_text(\"local_dependencies: [../setup_py_project]\")\n    r_extra = maybe_as_toml(toml_or_yaml, r_extra)\n\n    # Add a line to hatch_project local_dependencies which should\n    # make hatch_project depend on setup_py_project, via extra_project! However, extra_project is\n    # not `pip installable` so we're testing that path.\n    setup_py_project_req = example_folder / \"hatch_project\" / \"requirements.yaml\"\n    yaml = YAML(typ=\"safe\")\n    with setup_py_project_req.open(\"r\") as f:\n        requirements = yaml.load(f)\n    requirements[\"local_dependencies\"] = [\"../extra_project\"]\n    with setup_py_project_req.open(\"w\") as f:\n        yaml.dump(requirements, f)\n\n    found_files = find_requirements_files(example_folder)\n    assert len(found_files) == 6\n\n    local_dependencies = parse_local_dependencies(\n        *found_files,\n        check_pip_installable=True,\n        verbose=True,\n    )\n    assert local_dependencies\n    assert local_dependencies == {\n        example_folder / \"hatch_project\": [\n            example_folder / \"setup_py_project\",\n            example_folder / \"setuptools_project\",\n        ],\n        example_folder / \"setup_py_project\": [\n            example_folder / \"hatch_project\",\n            example_folder / \"setuptools_project\",\n        ],\n        example_folder / \"pyproject_toml_project\": [\n            example_folder / \"hatch_project\",\n            example_folder / \"setup_py_project\",\n            example_folder / \"setuptools_project\",\n        ],\n        example_folder / \"setuptools_project\": [\n            example_folder / \"hatch_project\",\n            example_folder / \"setup_py_project\",\n        ],\n        example_folder / \"extra_project\": [\n            example_folder / \"hatch_project\",\n            example_folder / \"setup_py_project\",\n            example_folder / \"setuptools_project\",\n        ],\n    }\n\n\ndef test_local_non_unidep_managed_dependency(tmp_path: Path) -> None:\n    project1 = tmp_path / \"project1\"\n    project1.mkdir(exist_ok=True, parents=True)\n    project2 = tmp_path / \"project2\"\n    project2.mkdir(exist_ok=True, parents=True)\n    r1 = project1 / \"requirements.yaml\"\n    r1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../project2  # is not managed by unidep\n            \"\"\",\n        ),\n    )\n    r2 = project2 / \"setup.py\"  # not managed by unidep\n    r2.touch()\n\n    requirements = parse_requirements(r1, verbose=True)  # This should not raise\n    assert requirements.requirements == {}\n\n    with pytest.warns(UserWarning, match=\"not managed by unidep\"):\n        data = parse_local_dependencies(r1, verbose=True)\n    assert data == {project1.resolve(): [project2.resolve()]}\n\n\ndef test_local_non_unidep_and_non_installable_managed_dependency(\n    tmp_path: Path,\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project1.mkdir(exist_ok=True, parents=True)\n    project2 = tmp_path / \"project2\"\n    project2.mkdir(exist_ok=True, parents=True)\n    r1 = project1 / \"requirements.yaml\"\n    r1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../project2  # is not managed by unidep and not installable\n            \"\"\",\n        ),\n    )\n    with pytest.raises(RuntimeError, match=\"is not pip installable\"):\n        parse_local_dependencies(r1, verbose=True)\n\n\ndef test_local_empty_git_submodule_dependency(\n    tmp_path: Path,\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project1.mkdir(exist_ok=True, parents=True)\n    project2 = tmp_path / \"project2\"\n    project2.mkdir(exist_ok=True, parents=True)\n    (project2 / \".git\").touch()\n\n    r1 = project1 / \"requirements.yaml\"\n    r1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../project2  # has only `.git` file\n            \"\"\",\n        ),\n    )\n    with pytest.raises(RuntimeError, match=\"is an empty Git submodule\"):\n        parse_local_dependencies(r1, verbose=True)\n\n\ndef test_parse_local_dependencies_missing(\n    tmp_path: Path,\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project1.mkdir(exist_ok=True, parents=True)\n    r1 = project1 / \"requirements.yaml\"\n    r1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../does-not-exist\n            \"\"\",\n        ),\n    )\n    with pytest.raises(FileNotFoundError, match=r\"not found\\.\"):\n        parse_local_dependencies(r1, verbose=True, raise_if_missing=True)\n\n    local_dependencies = parse_local_dependencies(\n        r1,\n        verbose=True,\n        raise_if_missing=False,\n    )\n    assert local_dependencies == {}\n\n\n@pytest.mark.parametrize(\"unidep_managed\", [True, False])\ndef test_parse_local_dependencies_without_local_deps_themselves(\n    tmp_path: Path,\n    unidep_managed: bool,  # noqa: FBT001\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project1.mkdir(exist_ok=True, parents=True)\n    r1 = project1 / \"requirements.yaml\"\n    r1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            local_dependencies:\n                - ../project2\n            \"\"\",\n        ),\n    )\n\n    project2 = tmp_path / \"project2\"\n    project2.mkdir(exist_ok=True, parents=True)\n    r2 = project2 / \"pyproject.toml\"\n    txt = textwrap.dedent(\n        \"\"\"\\\n            [build-system]\n            requires = [\"setuptools\", \"wheel\"]\n            \"\"\",\n    )\n    if unidep_managed:\n        txt += '[tool.unidep]\\ndependencies = [\"numpy\"]'\n    r2.write_text(txt)\n    ctx = (\n        pytest.warns(UserWarning, match=\"not managed by unidep\")\n        if not unidep_managed\n        else nullcontext()\n    )\n    with ctx:\n        local_dependencies = parse_local_dependencies(\n            r1,\n            verbose=True,\n            raise_if_missing=True,\n        )\n    assert local_dependencies == {project1: [project2]}\n\n    r2.write_text(\"\")\n    with pytest.raises(RuntimeError, match=\"is not pip installable\"):\n        parse_local_dependencies(r1, verbose=True, raise_if_missing=True)\n\n\ndef test_parse_requirements_unmanaged_local_dependency(tmp_path: Path) -> None:\n    \"\"\"Local dep without requirements.yaml hits the None branch in _add_local_dependencies.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir()\n    unmanaged = tmp_path / \"unmanaged\"\n    unmanaged.mkdir()\n    # pip-installable but not unidep-managed\n    (unmanaged / \"setup.py\").write_text(\n        \"from setuptools import setup; setup(name='unmanaged')\",\n    )\n    req = project / \"requirements.yaml\"\n    req.write_text(\n        textwrap.dedent(\"\"\"\\\n            dependencies:\n              - numpy\n            local_dependencies:\n              - ../unmanaged\n        \"\"\"),\n    )\n    result = parse_requirements(req, verbose=False)\n    # Parsing succeeds; unmanaged dep is silently skipped\n    assert \"numpy\" in result.requirements\n"
  },
  {
    "path": "tests/test_parse_yaml_nested_local_dependencies.py",
    "content": "\"\"\"Test parsing nested local dependencies from YAML files.\"\"\"\n\nfrom __future__ import annotations\n\nimport textwrap\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nimport pytest\n\nfrom unidep import (\n    parse_local_dependencies,\n    parse_requirements,\n)\n\nfrom .helpers import maybe_as_toml\n\nif TYPE_CHECKING:\n    import sys\n\n    if sys.version_info >= (3, 8):\n        from typing import Literal\n    else:  # pragma: no cover\n        from typing_extensions import Literal\n\n\nREPO_ROOT = Path(__file__).parent.parent\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_nested_local_dependencies_multiple_levels(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project2 = tmp_path / \"project2\"\n    project3 = tmp_path / \"project3\"\n    project4 = tmp_path / \"project4\"\n    for project in [project1, project2, project3, project4]:\n        project.mkdir(exist_ok=True, parents=True)\n        (project / \"setup.py\").touch()  # Make projects pip installable\n\n    r1 = project1 / \"requirements.yaml\"\n    r2 = project2 / \"requirements.yaml\"\n    r3 = project3 / \"requirements.yaml\"\n    r4 = project4 / \"requirements.yaml\"\n\n    r1.write_text(\n        textwrap.dedent(\"\"\"\n        dependencies:\n            - package1\n        local_dependencies:\n            - ../project2\n    \"\"\"),\n    )\n\n    r2.write_text(\n        textwrap.dedent(\"\"\"\n        dependencies:\n            - package2\n        local_dependencies:\n            - ../project3\n    \"\"\"),\n    )\n\n    r3.write_text(\n        textwrap.dedent(\"\"\"\n        dependencies:\n            - package3\n        local_dependencies:\n            - ../project4\n    \"\"\"),\n    )\n\n    r4.write_text(\n        textwrap.dedent(\"\"\"\n        dependencies:\n            - package4\n    \"\"\"),\n    )\n\n    r1 = maybe_as_toml(toml_or_yaml, r1)\n    r2 = maybe_as_toml(toml_or_yaml, r2)\n    r3 = maybe_as_toml(toml_or_yaml, r3)\n    r4 = maybe_as_toml(toml_or_yaml, r4)\n\n    local_dependencies = parse_local_dependencies(\n        r1,\n        verbose=True,\n        check_pip_installable=True,\n    )\n\n    assert local_dependencies == {\n        project1.resolve(): [\n            project2.resolve(),\n            project3.resolve(),\n            project4.resolve(),\n        ],\n    }\n\n    requirements = parse_requirements(r1, verbose=True)\n    assert set(requirements.requirements.keys()) == {\n        \"package1\",\n        \"package2\",\n        \"package3\",\n        \"package4\",\n    }\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_nested_local_dependencies_with_circular_reference(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project2 = tmp_path / \"project2\"\n    project3 = tmp_path / \"project3\"\n    for project in [project1, project2, project3]:\n        project.mkdir(exist_ok=True, parents=True)\n        (project / \"setup.py\").touch()  # Make projects pip installable\n\n    r1 = project1 / \"requirements.yaml\"\n    r2 = project2 / \"requirements.yaml\"\n    r3 = project3 / \"requirements.yaml\"\n\n    r1.write_text(\n        textwrap.dedent(\"\"\"\n        dependencies:\n            - package1\n        local_dependencies:\n            - ../project2\n    \"\"\"),\n    )\n\n    r2.write_text(\n        textwrap.dedent(\"\"\"\n        dependencies:\n            - package2\n        local_dependencies:\n            - ../project3\n    \"\"\"),\n    )\n\n    r3.write_text(\n        textwrap.dedent(\"\"\"\n        dependencies:\n            - package3\n        local_dependencies:\n            - ../project1\n    \"\"\"),\n    )\n\n    r1 = maybe_as_toml(toml_or_yaml, r1)\n    r2 = maybe_as_toml(toml_or_yaml, r2)\n    r3 = maybe_as_toml(toml_or_yaml, r3)\n\n    local_dependencies = parse_local_dependencies(\n        r1,\n        verbose=True,\n        check_pip_installable=True,\n    )\n\n    assert local_dependencies == {\n        project1.resolve(): [project2.resolve(), project3.resolve()],\n    }\n\n    requirements = parse_requirements(r1, verbose=True)\n    assert set(requirements.requirements.keys()) == {\"package1\", \"package2\", \"package3\"}\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_nested_local_dependencies_with_non_unidep_managed_project(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project2 = tmp_path / \"project2\"\n    project3 = tmp_path / \"project3\"\n    for project in [project1, project2]:\n        project.mkdir(exist_ok=True, parents=True)\n        (project / \"setup.py\").touch()  # Make projects pip installable\n\n    # Create project3 as a non-unidep managed project\n    project3.mkdir(exist_ok=True, parents=True)\n    (project3 / \"setup.py\").touch()  # Make it pip installable but not unidep managed\n\n    r1 = project1 / \"requirements.yaml\"\n    r2 = project2 / \"requirements.yaml\"\n\n    r1.write_text(\n        textwrap.dedent(\"\"\"\n        dependencies:\n            - package1\n        local_dependencies:\n            - ../project2\n    \"\"\"),\n    )\n\n    r2.write_text(\n        textwrap.dedent(\"\"\"\n        dependencies:\n            - package2\n        local_dependencies:\n            - ../project3\n    \"\"\"),\n    )\n\n    r1 = maybe_as_toml(toml_or_yaml, r1)\n    r2 = maybe_as_toml(toml_or_yaml, r2)\n\n    # project3 is non-unidep managed but pip installable\n\n    with pytest.warns(UserWarning, match=\"not managed by unidep\"):\n        local_dependencies = parse_local_dependencies(\n            r1,\n            verbose=True,\n            check_pip_installable=True,\n            warn_non_managed=True,\n        )\n\n    assert local_dependencies == {\n        project1.resolve(): [project2.resolve(), project3.resolve()],\n    }\n\n    # We don't expect a warning here anymore, as it should have been raised in parse_local_dependencies\n    requirements = parse_requirements(r1, verbose=True)\n\n    assert set(requirements.requirements.keys()) == {\"package1\", \"package2\"}\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_skip_propagates_to_nested_local_dependency(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    root = tmp_path / \"root\"\n    system = root / \"system\"\n    shared = root / \"shared\"\n    system.mkdir(parents=True, exist_ok=True)\n    shared.mkdir(parents=True, exist_ok=True)\n\n    root_req = root / \"requirements.yaml\"\n    system_req = system / \"requirements.yaml\"\n\n    root_req.write_text(\n        textwrap.dedent(\n            \"\"\"\n            local_dependencies:\n              - ./system\n              - local: ./shared\n                use: skip\n            \"\"\",\n        ),\n    )\n\n    system_req.write_text(\n        textwrap.dedent(\n            \"\"\"\n            local_dependencies:\n              - ../shared\n            \"\"\",\n        ),\n    )\n\n    root_req = maybe_as_toml(toml_or_yaml, root_req)\n    system_req = maybe_as_toml(toml_or_yaml, system_req)\n\n    requirements = parse_requirements(root_req)\n    assert \"shared\" not in requirements.requirements\n\n    local_dependencies = parse_local_dependencies(root_req, check_pip_installable=False)\n    assert local_dependencies == {root.resolve(): [system.resolve()]}\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_pypi_override_propagates_to_nested_local_dependency(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    root = tmp_path / \"root\"\n    system = root / \"system\"\n    shared = root / \"shared\"\n    system.mkdir(parents=True, exist_ok=True)\n    shared.mkdir(parents=True, exist_ok=True)\n\n    root_req = root / \"requirements.yaml\"\n    system_req = system / \"requirements.yaml\"\n\n    root_req.write_text(\n        textwrap.dedent(\n            \"\"\"\n            local_dependencies:\n              - ./system\n              - local: ./shared\n                pypi: company-shared>=1.0\n                use: pypi\n            \"\"\",\n        ),\n    )\n\n    system_req.write_text(\n        textwrap.dedent(\n            \"\"\"\n            local_dependencies:\n              - ../shared\n            \"\"\",\n        ),\n    )\n\n    root_req = maybe_as_toml(toml_or_yaml, root_req)\n    system_req = maybe_as_toml(toml_or_yaml, system_req)\n\n    requirements = parse_requirements(root_req)\n    assert \"company-shared\" in requirements.requirements\n\n    local_dependencies = parse_local_dependencies(root_req, check_pip_installable=False)\n    assert local_dependencies == {root.resolve(): [system.resolve()]}\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_skip_propagates_when_nested_entry_is_dict(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    root = tmp_path / \"root\"\n    system = root / \"system\"\n    shared = root / \"shared\"\n    system.mkdir(parents=True, exist_ok=True)\n    shared.mkdir(parents=True, exist_ok=True)\n\n    root_req = root / \"requirements.yaml\"\n    system_req = system / \"requirements.yaml\"\n\n    root_req.write_text(\n        textwrap.dedent(\n            \"\"\"\n            local_dependencies:\n              - ./system\n              - local: ./shared\n                use: skip\n            \"\"\",\n        ),\n    )\n\n    system_req.write_text(\n        textwrap.dedent(\n            \"\"\"\n            local_dependencies:\n              - local: ../shared\n            \"\"\",\n        ),\n    )\n\n    root_req = maybe_as_toml(toml_or_yaml, root_req)\n    system_req = maybe_as_toml(toml_or_yaml, system_req)\n\n    requirements = parse_requirements(root_req)\n    assert \"shared\" not in requirements.requirements\n\n    local_dependencies = parse_local_dependencies(root_req, check_pip_installable=False)\n    assert local_dependencies == {root.resolve(): [system.resolve()]}\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_nested_local_dependencies_with_extras(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    project1 = tmp_path / \"project1\"\n    project2 = tmp_path / \"project2\"\n    project3 = tmp_path / \"project3\"\n    for project in [project1, project2, project3]:\n        project.mkdir(exist_ok=True, parents=True)\n        (project / \"setup.py\").touch()  # Make projects pip installable\n\n    r1 = project1 / \"requirements.yaml\"\n    r2 = project2 / \"requirements.yaml\"\n    r3 = project3 / \"requirements.yaml\"\n\n    r1.write_text(\n        textwrap.dedent(\"\"\"\n        dependencies:\n            - package1\n        local_dependencies:\n            - ../project2[test,docs]\n        optional_dependencies:\n            dev:\n                - dev-package\n    \"\"\"),\n    )\n\n    r2.write_text(\n        textwrap.dedent(\"\"\"\n        dependencies:\n            - package2\n        local_dependencies:\n            - ../project3[full]\n        optional_dependencies:\n            test:\n                - pytest\n            docs:\n                - sphinx\n    \"\"\"),\n    )\n\n    r3.write_text(\n        textwrap.dedent(\"\"\"\n        dependencies:\n            - package3\n        optional_dependencies:\n            full:\n                - extra-package\n    \"\"\"),\n    )\n\n    r1 = maybe_as_toml(toml_or_yaml, r1)\n    r2 = maybe_as_toml(toml_or_yaml, r2)\n    r3 = maybe_as_toml(toml_or_yaml, r3)\n\n    local_dependencies = parse_local_dependencies(\n        Path(f\"{r1}[dev]\"),\n        verbose=True,\n        check_pip_installable=True,\n    )\n\n    assert local_dependencies == {\n        project1.resolve(): [project2.resolve(), project3.resolve()],\n    }\n\n    requirements = parse_requirements(r1, verbose=True, extras=[[\"dev\"]])\n    assert set(requirements.requirements.keys()) == {\n        \"package1\",\n        \"package2\",\n        \"package3\",\n        \"pytest\",\n        \"sphinx\",\n        \"extra-package\",\n    }\n\n    # Test with different extras\n    requirements_full = parse_requirements(r1, verbose=True, extras=[[\"dev\", \"full\"]])\n    assert set(requirements_full.requirements.keys()) == {\n        \"package1\",\n        \"package2\",\n        \"package3\",\n        \"pytest\",\n        \"sphinx\",\n        \"extra-package\",\n    }\n    assert requirements_full.optional_dependencies.keys() == {\"dev\"}\n    assert requirements_full.optional_dependencies[\"dev\"].keys() == {\"dev-package\"}\n"
  },
  {
    "path": "tests/test_pip_indices.py",
    "content": "\"\"\"Unit tests for pip_indices support in unidep.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path  # noqa: TC003\nfrom textwrap import dedent\n\nimport pytest\nimport yaml\n\nfrom unidep._conda_env import CondaEnvironmentSpec, write_conda_environment_file\nfrom unidep._dependencies_parsing import (\n    _collect_pip_indices,\n    parse_requirements,\n)\n\n\nclass TestPipIndicesParsing:\n    \"\"\"Test parsing of pip_indices from requirements.yaml and pyproject.toml.\"\"\"\n\n    def test_parse_pip_indices_from_yaml(self, tmp_path: Path) -> None:\n        \"\"\"Test parsing pip_indices from requirements.yaml.\"\"\"\n        requirements_file = tmp_path / \"requirements.yaml\"\n        requirements_file.write_text(\n            dedent(\n                \"\"\"\n                name: test_project\n                channels:\n                  - conda-forge\n                pip_indices:\n                  - https://pypi.org/simple/\n                  - https://private.company.com/simple/\n                dependencies:\n                  - numpy\n                  - pip: private-package\n                \"\"\",\n            ),\n        )\n\n        parsed = parse_requirements(requirements_file)\n        assert parsed.pip_indices == (\n            \"https://pypi.org/simple/\",\n            \"https://private.company.com/simple/\",\n        )\n\n    def test_parse_pip_indices_from_toml(self, tmp_path: Path) -> None:\n        \"\"\"Test parsing pip_indices from pyproject.toml.\"\"\"\n        pyproject_file = tmp_path / \"pyproject.toml\"\n        pyproject_file.write_text(\n            dedent(\n                \"\"\"\n                [tool.unidep]\n                channels = [\"conda-forge\"]\n                pip_indices = [\n                    \"https://pypi.org/simple/\",\n                    \"https://test.pypi.org/simple/\"\n                ]\n                dependencies = [\n                    \"numpy\",\n                    {pip = \"test-package\"}\n                ]\n                \"\"\",\n            ),\n        )\n\n        parsed = parse_requirements(pyproject_file)\n        assert parsed.pip_indices == (\n            \"https://pypi.org/simple/\",\n            \"https://test.pypi.org/simple/\",\n        )\n\n    def test_parse_empty_pip_indices(self, tmp_path: Path) -> None:\n        \"\"\"Test that missing pip_indices defaults to empty list.\"\"\"\n        requirements_file = tmp_path / \"requirements.yaml\"\n        requirements_file.write_text(\n            dedent(\n                \"\"\"\n                name: test_project\n                channels:\n                  - conda-forge\n                dependencies:\n                  - numpy\n                \"\"\",\n            ),\n        )\n\n        parsed = parse_requirements(requirements_file)\n        assert parsed.pip_indices == ()\n\n    def test_parse_pip_indices_with_env_vars(self, tmp_path: Path) -> None:\n        \"\"\"Test parsing pip_indices with environment variables.\"\"\"\n        requirements_file = tmp_path / \"requirements.yaml\"\n        requirements_file.write_text(\n            dedent(\n                \"\"\"\n                name: test_project\n                pip_indices:\n                  - https://${PIP_USER}:${PIP_PASSWORD}@private.company.com/simple/\n                  - https://pypi.org/simple/\n                dependencies:\n                  - pip: private-package\n                \"\"\",\n            ),\n        )\n\n        parsed = parse_requirements(requirements_file)\n        assert parsed.pip_indices == (\n            \"https://${PIP_USER}:${PIP_PASSWORD}@private.company.com/simple/\",\n            \"https://pypi.org/simple/\",\n        )\n\n    def test_merge_pip_indices_from_multiple_files(self, tmp_path: Path) -> None:\n        \"\"\"Test merging pip_indices from multiple requirements files.\"\"\"\n        # First requirements file\n        req1 = tmp_path / \"req1.yaml\"\n        req1.write_text(\n            dedent(\n                \"\"\"\n                name: project1\n                pip_indices:\n                  - https://pypi.org/simple/\n                  - https://index1.com/simple/\n                dependencies:\n                  - numpy\n                \"\"\",\n            ),\n        )\n\n        # Second requirements file\n        req2 = tmp_path / \"req2.yaml\"\n        req2.write_text(\n            dedent(\n                \"\"\"\n                name: project2\n                pip_indices:\n                  - https://index2.com/simple/\n                  - https://pypi.org/simple/  # Duplicate\n                dependencies:\n                  - pandas\n                \"\"\",\n            ),\n        )\n\n        # Parse and merge\n        parsed1 = parse_requirements(req1)\n        parsed2 = parse_requirements(req2)\n\n        # In real implementation, we'd have a merge function\n        # For now, test that both parse correctly\n        assert parsed1.pip_indices == (\n            \"https://pypi.org/simple/\",\n            \"https://index1.com/simple/\",\n        )\n        assert parsed2.pip_indices == (\n            \"https://index2.com/simple/\",\n            \"https://pypi.org/simple/\",\n        )\n\n    def test_pip_indices_ordering_preserved(self, tmp_path: Path) -> None:\n        \"\"\"Test that pip_indices order is preserved (first is primary).\"\"\"\n        requirements_file = tmp_path / \"requirements.yaml\"\n        indices = [\n            \"https://primary.com/simple/\",\n            \"https://secondary.com/simple/\",\n            \"https://tertiary.com/simple/\",\n        ]\n        requirements_file.write_text(\n            dedent(\n                f\"\"\"\n                name: test_project\n                pip_indices:\n                  - {indices[0]}\n                  - {indices[1]}\n                  - {indices[2]}\n                dependencies:\n                  - numpy\n                \"\"\",\n            ),\n        )\n\n        parsed = parse_requirements(requirements_file)\n        assert parsed.pip_indices == tuple(indices)\n        # First index should be treated as primary (--index-url)\n        assert parsed.pip_indices[0] == indices[0]\n\n    def test_collect_pip_indices_supports_single_string(self) -> None:\n        \"\"\"Test the string form of pip_indices.\"\"\"\n        indices = _collect_pip_indices(\n            {\"pip_indices\": \"https://pypi.org/simple/\"},\n        )\n\n        assert indices == [\"https://pypi.org/simple/\"]\n\n    def test_collect_pip_indices_rejects_invalid_value_type(self) -> None:\n        \"\"\"Test invalid top-level pip index values.\"\"\"\n        with pytest.raises(\n            TypeError,\n            match=\"`pip_indices` must be a string or a list of strings.\",\n        ):\n            _collect_pip_indices({\"pip_indices\": 123})\n\n    def test_collect_pip_indices_rejects_non_string_entries(self) -> None:\n        \"\"\"Test invalid pip index list entries.\"\"\"\n        with pytest.raises(\n            TypeError,\n            match=\"`pip_indices` entries must be strings.\",\n        ):\n            _collect_pip_indices(\n                {\"pip_indices\": [\"https://pypi.org/simple/\", 123]},\n            )\n\n\nclass TestEnvironmentGeneration:\n    \"\"\"Test generation of environment.yaml with pip_indices.\"\"\"\n\n    def test_environment_yaml_with_pip_indices(self, tmp_path: Path) -> None:\n        \"\"\"Test that pip_indices are included as pip-repositories in environment.yaml.\"\"\"\n        env_spec = CondaEnvironmentSpec(\n            channels=[\"conda-forge\"],\n            pip_indices=[\n                \"https://pypi.org/simple/\",\n                \"https://private.company.com/simple/\",\n            ],\n            platforms=[],\n            conda=[\"numpy\", \"pandas\"],\n            pip=[\"private-package\", \"requests\"],\n        )\n\n        env_file = tmp_path / \"environment.yaml\"\n        write_conda_environment_file(env_spec, env_file)\n\n        with env_file.open() as f:\n            env_dict = yaml.safe_load(f)\n\n        # Check that pip-repositories is included\n        assert \"pip-repositories\" in env_dict\n        assert env_dict[\"pip-repositories\"] == [\n            \"https://pypi.org/simple/\",\n            \"https://private.company.com/simple/\",\n        ]\n\n        # Check that dependencies structure is correct\n        assert \"dependencies\" in env_dict\n        deps = env_dict[\"dependencies\"]\n\n        # Find pip dependencies\n        pip_deps = None\n        for dep in deps:\n            if isinstance(dep, dict) and \"pip\" in dep:\n                pip_deps = dep[\"pip\"]\n                break\n\n        assert pip_deps is not None\n        assert \"private-package\" in pip_deps\n        assert \"requests\" in pip_deps\n\n    def test_environment_yaml_without_pip_indices(self, tmp_path: Path) -> None:\n        \"\"\"Test environment.yaml generation without pip_indices.\"\"\"\n        env_spec = CondaEnvironmentSpec(\n            channels=[\"conda-forge\"],\n            pip_indices=[],  # Empty pip_indices\n            platforms=[],\n            conda=[\"numpy\"],\n            pip=[\"requests\"],\n        )\n\n        env_file = tmp_path / \"environment.yaml\"\n        write_conda_environment_file(env_spec, env_file)\n\n        with env_file.open() as f:\n            env_dict = yaml.safe_load(f)\n\n        # pip-repositories should not be included if empty\n        assert \"pip-repositories\" not in env_dict\n\n    def test_environment_yaml_with_env_vars_in_indices(self, tmp_path: Path) -> None:\n        \"\"\"Test that environment variables in pip_indices are preserved.\"\"\"\n        env_spec = CondaEnvironmentSpec(\n            channels=[\"conda-forge\"],\n            pip_indices=[\n                \"https://${USER}:${PASS}@private.com/simple/\",\n                \"https://pypi.org/simple/\",\n            ],\n            platforms=[],\n            conda=[],\n            pip=[\"private-package\"],\n        )\n\n        env_file = tmp_path / \"environment.yaml\"\n        write_conda_environment_file(env_spec, env_file)\n\n        with env_file.open() as f:\n            content = f.read()\n            env_dict = yaml.safe_load(content)\n\n        # Environment variables should be preserved\n        assert (\n            env_dict[\"pip-repositories\"][0]\n            == \"https://${USER}:${PASS}@private.com/simple/\"\n        )\n\n\nclass TestPipCommandConstruction:\n    \"\"\"Test construction of pip install commands with indices.\"\"\"\n\n    def test_build_pip_command_with_indices(self) -> None:\n        \"\"\"Test building pip install command with index URLs.\"\"\"\n        pip_indices = [\n            \"https://pypi.org/simple/\",\n            \"https://private.company.com/simple/\",\n        ]\n\n        # Verify the logic: first index is primary, rest are extra\n        assert pip_indices[0] == \"https://pypi.org/simple/\"  # Primary\n        assert pip_indices[1] == \"https://private.company.com/simple/\"  # Extra\n\n    def test_build_pip_command_without_indices(self) -> None:\n        \"\"\"Test building pip install command without custom indices.\"\"\"\n        pip_indices: list[str] = []\n\n        assert len(pip_indices) == 0\n\n    def test_build_pip_command_single_index(self) -> None:\n        \"\"\"Test building pip install command with single index.\"\"\"\n        pip_indices = [\"https://custom.pypi.org/simple/\"]\n\n        assert len(pip_indices) == 1\n        assert pip_indices[0] == \"https://custom.pypi.org/simple/\"\n\n    def test_uv_compatibility(self) -> None:\n        \"\"\"Test that index flags are compatible with uv.\"\"\"\n        # uv uses the same --index-url and --extra-index-url flags as pip\n        pip_indices = [\n            \"https://pypi.org/simple/\",\n            \"https://test.pypi.org/simple/\",\n        ]\n\n        # Both pip and uv support these flags\n        pip_args = [\"--index-url\", pip_indices[0]]\n        uv_args = [\"--index-url\", pip_indices[0]]\n\n        assert pip_args == uv_args  # Same flags for both\n\n\nclass TestEdgeCases:\n    \"\"\"Test edge cases and error handling.\"\"\"\n\n    def test_invalid_url_format(self, tmp_path: Path) -> None:\n        \"\"\"Test handling of invalid URL formats.\"\"\"\n        requirements_file = tmp_path / \"requirements.yaml\"\n        requirements_file.write_text(\n            dedent(\n                \"\"\"\n                name: test_project\n                pip_indices:\n                  - not-a-valid-url\n                  - https://valid.url.com/simple/\n                dependencies:\n                  - numpy\n                \"\"\",\n            ),\n        )\n\n        # Should either validate and fail, or accept and let pip handle it\n        parsed = parse_requirements(requirements_file)\n        assert \"not-a-valid-url\" in parsed.pip_indices\n\n    def test_duplicate_indices(self, tmp_path: Path) -> None:\n        \"\"\"Test handling of duplicate indices.\"\"\"\n        requirements_file = tmp_path / \"requirements.yaml\"\n        requirements_file.write_text(\n            dedent(\n                \"\"\"\n                name: test_project\n                pip_indices:\n                  - https://pypi.org/simple/\n                  - https://private.com/simple/\n                  - https://pypi.org/simple/  # Duplicate\n                dependencies:\n                  - numpy\n                \"\"\",\n            ),\n        )\n\n        parsed = parse_requirements(requirements_file)\n        # The implementation deduplicates indices\n        assert len(parsed.pip_indices) == 2\n        assert parsed.pip_indices == (\n            \"https://pypi.org/simple/\",\n            \"https://private.com/simple/\",\n        )\n\n    def test_empty_string_in_indices(self, tmp_path: Path) -> None:\n        \"\"\"Test handling of empty strings in pip_indices.\"\"\"\n        requirements_file = tmp_path / \"requirements.yaml\"\n        requirements_file.write_text(\n            dedent(\n                \"\"\"\n                name: test_project\n                pip_indices:\n                  - \"\"\n                  - https://pypi.org/simple/\n                dependencies:\n                  - numpy\n                \"\"\",\n            ),\n        )\n\n        parsed = parse_requirements(requirements_file)\n        # Empty strings should be filtered out or raise an error\n        assert parsed.pip_indices  # Should have at least the valid URL\n\n    def test_missing_env_var_in_url(self, tmp_path: Path) -> None:\n        \"\"\"Test handling of missing environment variables.\"\"\"\n        requirements_file = tmp_path / \"requirements.yaml\"\n        requirements_file.write_text(\n            dedent(\n                \"\"\"\n                name: test_project\n                pip_indices:\n                  - https://${MISSING_VAR}@private.com/simple/\n                dependencies:\n                  - numpy\n                \"\"\",\n            ),\n        )\n\n        # Environment variable not set\n        if \"MISSING_VAR\" in os.environ:\n            del os.environ[\"MISSING_VAR\"]\n\n        parsed = parse_requirements(requirements_file)\n        # Should preserve the ${MISSING_VAR} syntax for later expansion\n        assert \"${MISSING_VAR}\" in parsed.pip_indices[0]\n"
  },
  {
    "path": "tests/test_pip_indices_cli.py",
    "content": "\"\"\"Tests for pip_indices CLI functionality to achieve 100% coverage.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path  # noqa: TC003\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom unidep._cli import _build_pip_index_arguments\nfrom unidep._conda_env import create_conda_env_specification\n\n\nclass TestBuildPipIndexArguments:\n    \"\"\"Test the _build_pip_index_arguments function.\"\"\"\n\n    def test_empty_indices(self) -> None:\n        \"\"\"Test with empty pip_indices list.\"\"\"\n        args = _build_pip_index_arguments([])\n        assert args == []\n\n    def test_single_index(self) -> None:\n        \"\"\"Test with a single index URL.\"\"\"\n        indices = [\"https://pypi.org/simple/\"]\n        args = _build_pip_index_arguments(indices)\n        assert args == [\"--index-url\", \"https://pypi.org/simple/\"]\n\n    def test_multiple_indices(self) -> None:\n        \"\"\"Test with multiple index URLs.\"\"\"\n        indices = [\n            \"https://pypi.org/simple/\",\n            \"https://test.pypi.org/simple/\",\n            \"https://private.com/simple/\",\n        ]\n        args = _build_pip_index_arguments(indices)\n        assert args == [\n            \"--index-url\",\n            \"https://pypi.org/simple/\",\n            \"--extra-index-url\",\n            \"https://test.pypi.org/simple/\",\n            \"--extra-index-url\",\n            \"https://private.com/simple/\",\n        ]\n\n    def test_environment_variable_expansion(self) -> None:\n        \"\"\"Test that environment variables are expanded in URLs.\"\"\"\n        # Set environment variables\n        os.environ[\"PIP_USER\"] = \"testuser\"\n        os.environ[\"PIP_PASSWORD\"] = \"testpass\"  # noqa: S105\n\n        try:\n            indices = [\n                \"https://${PIP_USER}:${PIP_PASSWORD}@private.com/simple/\",\n                \"https://public.com/simple/\",\n            ]\n            args = _build_pip_index_arguments(indices)\n\n            assert args == [\n                \"--index-url\",\n                \"https://testuser:testpass@private.com/simple/\",\n                \"--extra-index-url\",\n                \"https://public.com/simple/\",\n            ]\n        finally:\n            # Clean up\n            del os.environ[\"PIP_USER\"]\n            del os.environ[\"PIP_PASSWORD\"]\n\n    def test_missing_environment_variable(self) -> None:\n        \"\"\"Test handling of missing environment variables.\"\"\"\n        # Ensure the variable is not set\n        os.environ.pop(\"NONEXISTENT_VAR\", None)\n\n        indices = [\"https://${NONEXISTENT_VAR}@private.com/simple/\"]\n        args = _build_pip_index_arguments(indices)\n\n        # expandvars leaves the ${VAR} as-is if not found\n        assert args == [\"--index-url\", \"https://${NONEXISTENT_VAR}@private.com/simple/\"]\n\n    def test_complex_environment_variables(self) -> None:\n        \"\"\"Test complex environment variable patterns.\"\"\"\n        os.environ[\"DOMAIN\"] = \"example.com\"\n        os.environ[\"PORT\"] = \"8080\"\n\n        try:\n            indices = [\n                \"https://${DOMAIN}:${PORT}/simple/\",\n                \"https://backup.${DOMAIN}/simple/\",\n            ]\n            args = _build_pip_index_arguments(indices)\n\n            assert args == [\n                \"--index-url\",\n                \"https://example.com:8080/simple/\",\n                \"--extra-index-url\",\n                \"https://backup.example.com/simple/\",\n            ]\n        finally:\n            del os.environ[\"DOMAIN\"]\n            del os.environ[\"PORT\"]\n\n\nclass TestPipInstallLocalWithIndices:\n    \"\"\"Test pip install with custom indices.\"\"\"\n\n    @patch(\"unidep._cli.subprocess.run\")\n    @patch(\"unidep._cli.shutil.which\")\n    def test_pip_install_with_indices(\n        self,\n        mock_which: MagicMock,\n        mock_run: MagicMock,\n    ) -> None:\n        \"\"\"Test that pip install uses the correct index arguments.\"\"\"\n        from unidep._cli import _pip_install_local\n\n        mock_which.return_value = \"/usr/bin/pip\"\n        mock_run.return_value = MagicMock(returncode=0)\n\n        # Call with pip_indices\n        _pip_install_local(\n            \"test_package\",\n            editable=False,\n            dry_run=False,\n            python_executable=\"/usr/bin/python\",\n            conda_run=[],\n            no_uv=True,\n            pip_indices=[\"https://pypi.org/simple/\", \"https://test.pypi.org/simple/\"],\n            flags=[\"--no-deps\"],\n        )\n\n        # Verify the command includes index arguments\n        call_args = mock_run.call_args[0][0]\n        assert \"--index-url\" in call_args\n        assert \"https://pypi.org/simple/\" in call_args\n        assert \"--extra-index-url\" in call_args\n        assert \"https://test.pypi.org/simple/\" in call_args\n\n    @patch(\"unidep._cli.subprocess.run\")\n    @patch(\"unidep._cli.shutil.which\")\n    def test_uv_install_with_indices(\n        self,\n        mock_which: MagicMock,\n        mock_run: MagicMock,\n    ) -> None:\n        \"\"\"Test that uv install uses the correct index arguments.\"\"\"\n        from unidep._cli import _pip_install_local\n\n        # Mock uv as the installer\n        def which_side_effect(cmd: str) -> str | None:\n            if cmd == \"uv\":\n                return \"/usr/bin/uv\"\n            return None\n\n        mock_which.side_effect = which_side_effect\n        mock_run.return_value = MagicMock(returncode=0)\n\n        # Call with pip_indices\n        _pip_install_local(\n            \"test_package\",\n            editable=False,\n            dry_run=False,\n            python_executable=\"/usr/bin/python\",\n            conda_run=[],\n            no_uv=False,  # Enable uv\n            pip_indices=[\"https://private.com/simple/\"],\n            flags=[\"--no-deps\"],\n        )\n\n        # Verify uv command includes index arguments\n        call_args = mock_run.call_args[0][0]\n        assert \"uv\" in call_args\n        assert \"pip\" in call_args\n        assert \"install\" in call_args\n        assert \"--index-url\" in call_args\n        assert \"https://private.com/simple/\" in call_args\n\n\nclass TestCondaEnvWithPipRepositories:\n    \"\"\"Test conda environment generation with pip-repositories.\"\"\"\n\n    def test_write_env_with_pip_repositories(self, tmp_path: Path) -> None:\n        \"\"\"Test that `pip-repositories` is written to environment.yaml.\"\"\"\n        from unidep._conda_env import CondaEnvironmentSpec, write_conda_environment_file\n\n        env_spec = CondaEnvironmentSpec(\n            channels=[\"conda-forge\"],\n            pip_indices=[\n                \"https://pypi.org/simple/\",\n                \"https://private.company.com/simple/\",\n            ],\n            platforms=[\"linux-64\"],\n            conda=[\"python=3.11\"],\n            pip=[\"requests\"],\n        )\n\n        env_file = tmp_path / \"environment.yaml\"\n        write_conda_environment_file(env_spec, env_file, name=\"test_env\")\n\n        content = env_file.read_text()\n        assert \"pip-repositories:\" in content\n        assert \"https://pypi.org/simple/\" in content\n        assert \"https://private.company.com/simple/\" in content\n\n        # Verify order is preserved\n        lines = content.split(\"\\n\")\n        repo_lines = [\n            line for line in lines if \"https://\" in line and \"simple/\" in line\n        ]\n        assert \"pypi.org\" in repo_lines[0]\n        assert \"private.company.com\" in repo_lines[1]\n\n    def test_write_env_without_pip_repositories(self, tmp_path: Path) -> None:\n        \"\"\"Test environment.yaml without `pip-repositories` when the list is empty.\"\"\"\n        from unidep._conda_env import CondaEnvironmentSpec, write_conda_environment_file\n\n        env_spec = CondaEnvironmentSpec(\n            channels=[\"conda-forge\"],\n            pip_indices=[],  # Empty list\n            platforms=[\"linux-64\"],\n            conda=[\"python=3.11\"],\n            pip=[\"requests\"],\n        )\n\n        env_file = tmp_path / \"environment.yaml\"\n        write_conda_environment_file(env_spec, env_file, name=\"test_env\")\n\n        content = env_file.read_text()\n        assert \"pip-repositories:\" not in content\n\n\nclass TestCreateCondaEnvSpecificationCompatibility:\n    \"\"\"Test compatibility paths in create_conda_env_specification.\"\"\"\n\n    def test_accepts_string_keyword_pip_indices(self) -> None:\n        \"\"\"Test pip_indices passed as a single string keyword.\"\"\"\n        env_spec = create_conda_env_specification(\n            [],\n            [],\n            platforms=[\"linux-64\"],\n            pip_indices=\"https://pypi.org/simple/\",\n        )\n\n        assert env_spec.platforms == [\"linux-64\"]\n        assert env_spec.pip_indices == (\"https://pypi.org/simple/\",)\n\n    def test_accepts_legacy_positional_selector(self) -> None:\n        \"\"\"Test the older positional selector calling convention.\"\"\"\n        env_spec = create_conda_env_specification([], [], [\"linux-64\"], \"comment\")\n\n        assert env_spec.platforms == [\"linux-64\"]\n        assert env_spec.pip_indices == ()\n\n    def test_accepts_legacy_positional_pip_indices_and_selector(self) -> None:\n        \"\"\"Test the fully positional legacy calling convention.\"\"\"\n        env_spec = create_conda_env_specification(\n            [],\n            [],\n            [\"https://pypi.org/simple/\"],\n            [\"linux-64\"],\n            \"comment\",\n        )\n\n        assert env_spec.platforms == [\"linux-64\"]\n        assert env_spec.pip_indices == (\"https://pypi.org/simple/\",)\n\n    def test_rejects_missing_platforms_argument(self) -> None:\n        \"\"\"Test that platforms remain required.\"\"\"\n        with pytest.raises(TypeError, match=\"Missing required `platforms` argument.\"):\n            create_conda_env_specification([], [])\n\n    def test_rejects_too_many_positionals_with_platforms_keyword(self) -> None:\n        \"\"\"Test too many positional arguments when platforms is keyword-only.\"\"\"\n        with pytest.raises(\n            TypeError,\n            match=\"Too many positional arguments\",\n        ):\n            create_conda_env_specification(\n                [],\n                [],\n                [\"https://pypi.org/simple/\"],\n                \"comment\",\n                platforms=[\"linux-64\"],\n            )\n\n    def test_rejects_duplicate_pip_indices_with_platforms_keyword(self) -> None:\n        \"\"\"Test duplicate positional and keyword pip_indices.\"\"\"\n        with pytest.raises(\n            TypeError,\n            match=\"`pip_indices` was provided both positionally and by keyword.\",\n        ):\n            create_conda_env_specification(\n                [],\n                [],\n                [\"https://pypi.org/simple/\"],\n                platforms=[\"linux-64\"],\n                pip_indices=[\"https://test.pypi.org/simple/\"],\n            )\n\n    def test_rejects_duplicate_pip_indices_in_legacy_two_argument_form(self) -> None:\n        \"\"\"Test duplicate pip_indices in the legacy two-argument form.\"\"\"\n        with pytest.raises(\n            TypeError,\n            match=\"`pip_indices` was provided both positionally and by keyword.\",\n        ):\n            create_conda_env_specification(\n                [],\n                [],\n                [\"https://pypi.org/simple/\"],\n                [\"linux-64\"],\n                pip_indices=[\"https://test.pypi.org/simple/\"],\n            )\n\n    def test_rejects_duplicate_pip_indices_in_legacy_three_argument_form(\n        self,\n    ) -> None:\n        \"\"\"Test duplicate pip_indices in the legacy three-argument form.\"\"\"\n        with pytest.raises(\n            TypeError,\n            match=\"`pip_indices` was provided both positionally and by keyword.\",\n        ):\n            create_conda_env_specification(\n                [],\n                [],\n                [\"https://pypi.org/simple/\"],\n                [\"linux-64\"],\n                \"comment\",\n                pip_indices=[\"https://test.pypi.org/simple/\"],\n            )\n\n    def test_rejects_too_many_legacy_positional_arguments(self) -> None:\n        \"\"\"Test too many positional arguments in the legacy form.\"\"\"\n        with pytest.raises(\n            TypeError,\n            match=\"Too many positional arguments\",\n        ):\n            create_conda_env_specification(\n                [],\n                [],\n                [\"https://pypi.org/simple/\"],\n                [\"linux-64\"],\n                \"comment\",\n                \"extra\",\n            )\n\n\nclass TestInstallCommandWithIndices:\n    \"\"\"Test the install command with pip_indices.\"\"\"\n\n    @patch(\"unidep._cli.subprocess.run\")\n    @patch(\"unidep._cli._maybe_conda_executable\")\n    @patch(\"unidep._cli._use_uv\")\n    def test_install_command_with_pip_indices(\n        self,\n        mock_use_uv: MagicMock,\n        mock_conda: MagicMock,\n        mock_run: MagicMock,\n        tmp_path: Path,\n    ) -> None:\n        \"\"\"Test install command properly passes pip_indices to pip install.\"\"\"\n        from unidep._cli import _install_command\n\n        # Setup mocks\n        mock_use_uv.return_value = False  # Don't use uv\n        mock_conda.return_value = None  # No conda\n        mock_run.return_value = MagicMock(returncode=0)\n\n        # Create a requirements file with pip_indices\n        req_file = tmp_path / \"requirements.yaml\"\n        req_file.write_text(\"\"\"\nname: test_project\npip_indices:\n  - https://pypi.org/simple/\n  - https://private.com/simple/\ndependencies:\n  - pip: requests\n  - pip: private-package\n\"\"\")\n\n        # Run install command\n        _install_command(\n            req_file,\n            conda_executable=None,\n            conda_env_name=None,\n            conda_env_prefix=None,\n            conda_lock_file=None,\n            dry_run=False,\n            editable=False,\n            skip_local=True,\n            skip_pip=False,\n            skip_conda=True,\n            no_dependencies=False,\n            no_uv=True,\n            verbose=False,\n        )\n\n        # Check that pip was called with index arguments\n        pip_call_found = False\n        for call in mock_run.call_args_list:\n            args = call[0][0] if call[0] else []\n            if \"pip\" in args and \"install\" in args:\n                pip_call_found = True\n                assert \"--index-url\" in args\n                assert \"https://pypi.org/simple/\" in args\n                assert \"--extra-index-url\" in args\n                assert \"https://private.com/simple/\" in args\n                break\n\n        assert pip_call_found, \"pip install was not called with indices\"\n\n    @patch(\"unidep._cli.subprocess.run\")\n    @patch(\"unidep._cli._maybe_conda_executable\")\n    @patch(\"unidep._cli._use_uv\")\n    def test_install_command_with_uv_and_indices(\n        self,\n        mock_use_uv: MagicMock,\n        mock_conda: MagicMock,\n        mock_run: MagicMock,\n        tmp_path: Path,\n    ) -> None:\n        \"\"\"Test install command with uv properly passes pip_indices.\"\"\"\n        from unidep._cli import _install_command\n\n        # Setup mocks\n        mock_use_uv.return_value = True  # Use uv\n        mock_conda.return_value = None  # No conda\n        mock_run.return_value = MagicMock(returncode=0)\n\n        # Create a requirements file with pip_indices\n        req_file = tmp_path / \"requirements.yaml\"\n        req_file.write_text(\"\"\"\nname: test_project\npip_indices:\n  - https://private.com/simple/\ndependencies:\n  - pip: private-package\n\"\"\")\n\n        # Run install command\n        _install_command(\n            req_file,\n            conda_executable=None,\n            conda_env_name=None,\n            conda_env_prefix=None,\n            conda_lock_file=None,\n            dry_run=False,\n            editable=False,\n            skip_local=True,\n            skip_pip=False,\n            skip_conda=True,\n            no_dependencies=False,\n            no_uv=False,  # Allow uv\n            verbose=False,\n        )\n\n        # Check that uv was called with index arguments\n        uv_call_found = False\n        for call in mock_run.call_args_list:\n            args = call[0][0] if call[0] else []\n            if \"uv\" in args and \"pip\" in args and \"install\" in args:\n                uv_call_found = True\n                assert \"--index-url\" in args\n                assert \"https://private.com/simple/\" in args\n                break\n\n        assert uv_call_found, \"uv pip install was not called with indices\"\n\n\nclass TestPipIndicesIntegration:\n    \"\"\"Integration tests for pip_indices throughout the workflow.\"\"\"\n\n    def test_full_workflow_with_indices(self, tmp_path: Path) -> None:\n        \"\"\"Test complete workflow from parsing to environment generation.\"\"\"\n        from unidep._conda_env import (\n            create_conda_env_specification,\n            write_conda_environment_file,\n        )\n        from unidep._dependencies_parsing import parse_requirements\n\n        # Create a requirements file with pip_indices\n        req_file = tmp_path / \"requirements.yaml\"\n        req_file.write_text(\"\"\"\nname: test_project\nchannels:\n  - conda-forge\npip_indices:\n  - https://pypi.org/simple/\n  - https://test.pypi.org/simple/\ndependencies:\n  - python=3.11\n  - pip: requests\n  - pip: pytest\nplatforms:\n  - linux-64\n  - osx-arm64\n\"\"\")\n\n        # Parse requirements\n        parsed = parse_requirements(req_file)\n        assert len(parsed.pip_indices) == 2\n        assert parsed.pip_indices[0] == \"https://pypi.org/simple/\"\n        assert parsed.pip_indices[1] == \"https://test.pypi.org/simple/\"\n\n        # Create conda env specification\n        env_spec = create_conda_env_specification(\n            parsed.dependency_entries,\n            parsed.channels,\n            parsed.platforms,\n            pip_indices=parsed.pip_indices,\n        )\n\n        assert env_spec.pip_indices == parsed.pip_indices\n\n        # Write environment file\n        env_file = tmp_path / \"environment.yaml\"\n        write_conda_environment_file(env_spec, env_file)\n\n        # Verify the output\n        content = env_file.read_text()\n        assert \"pip-repositories:\" in content\n        assert \"- https://pypi.org/simple/\" in content\n        assert \"- https://test.pypi.org/simple/\" in content\n\n    @patch(\"unidep._conda_lock.conda_lock_command\")\n    def test_conda_lock_with_pip_indices(\n        self,\n        mock_conda_lock: MagicMock,\n        tmp_path: Path,\n    ) -> None:\n        \"\"\"Test that conda-lock properly includes pip_indices.\"\"\"\n        from unidep._conda_lock import conda_lock_command\n\n        # Create requirements file with pip_indices\n        req_file = tmp_path / \"requirements.yaml\"\n        req_file.write_text(\"\"\"\nname: test\nchannels:\n  - conda-forge\npip_indices:\n  - https://pypi.org/simple/\n  - https://private.com/simple/\ndependencies:\n  - numpy\n  - pip: requests\n\"\"\")\n\n        # Run conda-lock command (mocked)\n        conda_lock_command(\n            depth=1,\n            directory=tmp_path,\n            files=None,\n            platforms=[\"linux-64\"],\n            verbose=False,\n            only_global=False,\n            ignore_pins=[],\n            skip_dependencies=[],\n            overwrite_pins=[],\n            check_input_hash=False,\n            extra_flags=[],\n            lockfile=str(tmp_path / \"conda-lock.yml\"),\n        )\n\n        # Verify that the mock was called and pip_indices were passed through\n        assert mock_conda_lock.called\n\n    def test_merge_command_with_indices(self, tmp_path: Path) -> None:\n        \"\"\"Test unidep merge command with pip_indices.\"\"\"\n        from unidep._cli import _merge_command\n\n        # Create requirements file\n        req_file = tmp_path / \"requirements.yaml\"\n        req_file.write_text(\"\"\"\nname: test\nchannels:\n  - conda-forge\npip_indices:\n  - https://private.com/simple/\ndependencies:\n  - numpy\n\"\"\")\n\n        output_file = tmp_path / \"environment.yaml\"\n\n        # Run merge command\n        _merge_command(\n            depth=1,\n            directory=tmp_path,\n            files=[req_file],\n            name=\"merged_env\",\n            output=output_file,\n            stdout=False,\n            selector=\"sel\",\n            platforms=[],\n            ignore_pins=[],\n            skip_dependencies=[],\n            overwrite_pins=[],\n            verbose=False,\n        )\n\n        # Check output file\n        assert output_file.exists()\n        content = output_file.read_text()\n        assert \"pip-repositories:\" in content\n        assert \"https://private.com/simple/\" in content\n"
  },
  {
    "path": "tests/test_pip_indices_integration.py",
    "content": "\"\"\"End-to-end integration tests for pip_indices support in unidep.\"\"\"\n\nimport os\nfrom pathlib import Path\nfrom textwrap import dedent\nfrom typing import Any\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\n\nclass TestUnidepInstallIntegration:\n    \"\"\"Integration tests for unidep install with pip_indices.\"\"\"\n\n    @pytest.fixture\n    def mock_project(self, tmp_path: Path) -> Path:\n        \"\"\"Create a mock project with pip_indices configuration.\"\"\"\n        project_dir = tmp_path / \"test_project\"\n        project_dir.mkdir()\n\n        # Create requirements.yaml with pip_indices\n        requirements_file = project_dir / \"requirements.yaml\"\n        requirements_file.write_text(\n            dedent(\n                \"\"\"\n                name: test_project\n                channels:\n                  - conda-forge\n                pip_indices:\n                  - https://pypi.org/simple/\n                  - https://test.pypi.org/simple/\n                dependencies:\n                  - numpy\n                  - pip: requests\n                  - pip: test-package  # From test.pypi.org\n                \"\"\",\n            ),\n        )\n\n        # Create a simple setup.py\n        setup_file = project_dir / \"setup.py\"\n        setup_file.write_text(\n            dedent(\n                \"\"\"\n                from setuptools import setup, find_packages\n                setup(\n                    name=\"test_project\",\n                    version=\"0.1.0\",\n                    packages=find_packages(),\n                )\n                \"\"\",\n            ),\n        )\n\n        # Create package directory\n        (project_dir / \"test_project\").mkdir()\n        (project_dir / \"test_project\" / \"__init__.py\").touch()\n\n        return project_dir\n\n    @patch(\"subprocess.run\")\n    def test_install_with_pip_indices(self, mock_run: Any, mock_project: Path) -> None:  # noqa: ARG002\n        \"\"\"Test that unidep install uses pip_indices correctly.\"\"\"\n        mock_run.return_value = MagicMock(returncode=0, stdout=\"\", stderr=\"\")\n\n        # Simulate running unidep install\n\n        # Mock the install command execution\n        with patch(\"unidep._cli._pip_install_local\") as mock_pip_install:\n            mock_pip_install.return_value = None\n\n            # This would be the actual command execution\n            # For now, verify the expected behavior\n            expected_pip_args = [\n                \"--index-url\",\n                \"https://pypi.org/simple/\",\n                \"--extra-index-url\",\n                \"https://test.pypi.org/simple/\",\n            ]\n\n            # The actual implementation would construct these args\n            assert expected_pip_args[0] == \"--index-url\"\n            assert expected_pip_args[2] == \"--extra-index-url\"\n\n    @patch(\"subprocess.run\")\n    def test_install_with_env_var_indices(self, mock_run: Any, tmp_path: Path) -> None:\n        \"\"\"Test that environment variables in pip_indices are expanded.\"\"\"\n        project_dir = tmp_path / \"test_project\"\n        project_dir.mkdir()\n\n        # Set environment variables\n        os.environ[\"PIP_USER\"] = \"testuser\"\n        os.environ[\"PIP_PASSWORD\"] = \"testpass\"  # noqa: S105\n\n        requirements_file = project_dir / \"requirements.yaml\"\n        requirements_file.write_text(\n            dedent(\n                \"\"\"\n                name: test_project\n                pip_indices:\n                  - https://${PIP_USER}:${PIP_PASSWORD}@private.pypi.org/simple/\n                  - https://pypi.org/simple/\n                dependencies:\n                  - pip: private-package\n                \"\"\",\n            ),\n        )\n\n        mock_run.return_value = MagicMock(returncode=0)\n\n        # In actual implementation, env vars would be expanded\n        expected_url = \"https://testuser:testpass@private.pypi.org/simple/\"\n\n        # Verify env var expansion logic\n        url = \"https://${PIP_USER}:${PIP_PASSWORD}@private.pypi.org/simple/\"\n        expanded = url.replace(\"${PIP_USER}\", os.environ[\"PIP_USER\"])\n        expanded = expanded.replace(\"${PIP_PASSWORD}\", os.environ[\"PIP_PASSWORD\"])\n        assert expanded == expected_url\n\n        # Clean up env vars\n        del os.environ[\"PIP_USER\"]\n        del os.environ[\"PIP_PASSWORD\"]\n\n    def test_install_with_uv_backend(self, mock_project: Path) -> None:  # noqa: ARG002\n        \"\"\"Test that pip_indices work with uv backend.\"\"\"\n        # uv uses the same --index-url and --extra-index-url flags\n        with patch(\"shutil.which\", return_value=\"/path/to/uv\"), patch(\n            \"subprocess.run\",\n        ) as mock_run:\n            mock_run.return_value = MagicMock(returncode=0)\n\n            # Expected uv command structure\n            expected_args = [\n                \"uv\",\n                \"pip\",\n                \"install\",\n                \"--index-url\",\n                \"https://pypi.org/simple/\",\n                \"--extra-index-url\",\n                \"https://test.pypi.org/simple/\",\n            ]\n\n            # Verify uv compatibility\n            assert \"--index-url\" in expected_args\n            assert \"--extra-index-url\" in expected_args\n\n    def test_install_without_pip_indices(self, tmp_path: Path) -> None:\n        \"\"\"Test that unidep install works without pip_indices.\"\"\"\n        project_dir = tmp_path / \"test_project\"\n        project_dir.mkdir()\n\n        requirements_file = project_dir / \"requirements.yaml\"\n        requirements_file.write_text(\n            dedent(\n                \"\"\"\n                name: test_project\n                channels:\n                  - conda-forge\n                dependencies:\n                  - numpy\n                  - pip: requests\n                \"\"\",\n            ),\n        )\n\n        with patch(\"subprocess.run\") as mock_run:\n            mock_run.return_value = MagicMock(returncode=0)\n\n            # No index flags should be added\n            # Command should work with default PyPI\n            assert True  # Placeholder for actual test\n\n\nclass TestUnidepCondaLockIntegration:\n    \"\"\"Integration tests for unidep conda-lock with pip_indices.\"\"\"\n\n    @pytest.fixture\n    def mock_monorepo(self, tmp_path: Path) -> Path:\n        \"\"\"Create a mock monorepo with multiple projects using pip_indices.\"\"\"\n        monorepo = tmp_path / \"monorepo\"\n        monorepo.mkdir()\n\n        # Project 1 with pip_indices\n        proj1 = monorepo / \"project1\"\n        proj1.mkdir()\n        (proj1 / \"requirements.yaml\").write_text(\n            dedent(\n                \"\"\"\n                name: project1\n                channels:\n                  - conda-forge\n                pip_indices:\n                  - https://pypi.org/simple/\n                  - https://private1.com/simple/\n                dependencies:\n                  - numpy\n                  - pip: private-package1\n                \"\"\",\n            ),\n        )\n\n        # Project 2 with different pip_indices\n        proj2 = monorepo / \"project2\"\n        proj2.mkdir()\n        (proj2 / \"requirements.yaml\").write_text(\n            dedent(\n                \"\"\"\n                name: project2\n                channels:\n                  - conda-forge\n                pip_indices:\n                  - https://pypi.org/simple/\n                  - https://private2.com/simple/\n                dependencies:\n                  - pandas\n                  - pip: private-package2\n                \"\"\",\n            ),\n        )\n\n        return monorepo\n\n    def test_conda_lock_generates_pip_repositories(self, mock_monorepo: Path) -> None:\n        \"\"\"Test that conda-lock generates environment.yaml with `pip-repositories`.\"\"\"\n        _ = mock_monorepo  # Used to ensure fixture is called\n        with patch(\"subprocess.run\") as mock_run:\n            mock_run.return_value = MagicMock(returncode=0)\n\n            # Expected environment.yaml structure\n            expected_env = {\n                \"name\": \"myenv\",\n                \"channels\": [\"conda-forge\"],\n                \"pip-repositories\": [\n                    \"https://pypi.org/simple/\",\n                    \"https://private1.com/simple/\",\n                    \"https://private2.com/simple/\",\n                ],\n                \"dependencies\": [\n                    \"numpy\",\n                    \"pandas\",\n                    {\"pip\": [\"private-package1\", \"private-package2\"]},\n                ],\n            }\n\n            # Verify the structure\n            assert \"pip-repositories\" in expected_env\n            assert len(expected_env[\"pip-repositories\"]) == 3\n\n    def test_conda_lock_with_merged_indices(self, mock_monorepo: Path) -> None:  # noqa: ARG002\n        \"\"\"Test that conda-lock merges pip_indices from multiple projects.\"\"\"\n        with patch(\"unidep._conda_lock.conda_lock_command\") as mock_conda_lock:\n            mock_conda_lock.return_value = None\n\n            # Expected merged pip_indices (deduplicated)\n            expected_indices = [\n                \"https://pypi.org/simple/\",  # Common to both\n                \"https://private1.com/simple/\",  # From project1\n                \"https://private2.com/simple/\",  # From project2\n            ]\n\n            # Verify deduplication logic\n            all_indices = [\n                \"https://pypi.org/simple/\",\n                \"https://private1.com/simple/\",\n                \"https://pypi.org/simple/\",  # Duplicate\n                \"https://private2.com/simple/\",\n            ]\n            deduplicated = list(dict.fromkeys(all_indices))  # Preserve order\n            assert deduplicated == expected_indices\n\n    def test_conda_lock_creates_valid_lockfile(self, tmp_path: Path) -> None:\n        \"\"\"Test that conda-lock creates a valid lock file with pip-repositories.\"\"\"\n        project_dir = tmp_path / \"test_project\"\n        project_dir.mkdir()\n\n        requirements_file = project_dir / \"requirements.yaml\"\n        requirements_file.write_text(\n            dedent(\n                \"\"\"\n                name: test_project\n                channels:\n                  - conda-forge\n                pip_indices:\n                  - https://pypi.org/simple/\n                  - https://custom.pypi.org/simple/\n                dependencies:\n                  - python=3.11\n                  - pip: custom-package\n                \"\"\",\n            ),\n        )\n\n        # Mock conda-lock execution\n        with patch(\"subprocess.run\") as mock_run:\n            # First call generates environment.yaml\n            # Second call runs conda-lock\n            mock_run.return_value = MagicMock(returncode=0)\n\n            # Verify that the generated environment.yaml includes pip-repositories\n\n            env_content = {\n                \"name\": \"test_project\",\n                \"channels\": [\"conda-forge\"],\n                \"pip-repositories\": [\n                    \"https://pypi.org/simple/\",\n                    \"https://custom.pypi.org/simple/\",\n                ],\n                \"dependencies\": [\n                    \"python=3.11\",\n                    {\"pip\": [\"custom-package\"]},\n                ],\n            }\n\n            # Verify structure for conda-lock compatibility\n            assert \"pip-repositories\" in env_content\n            assert isinstance(env_content[\"pip-repositories\"], list)\n\n\nclass TestErrorHandling:\n    \"\"\"Test error handling and edge cases in integration.\"\"\"\n\n    def test_install_with_unreachable_index(self, tmp_path: Path) -> None:\n        \"\"\"Test behavior when a pip index is unreachable.\"\"\"\n        project_dir = tmp_path / \"test_project\"\n        project_dir.mkdir()\n\n        requirements_file = project_dir / \"requirements.yaml\"\n        requirements_file.write_text(\n            dedent(\n                \"\"\"\n                name: test_project\n                pip_indices:\n                  - https://unreachable.invalid.com/simple/\n                  - https://pypi.org/simple/\n                dependencies:\n                  - pip: numpy  # Should fall back to pypi.org\n                \"\"\",\n            ),\n        )\n\n        # Test that installation can continue with fallback\n        with patch(\"subprocess.run\") as mock_run:\n            # First attempt might fail, but should retry with pypi.org\n            mock_run.return_value = MagicMock(returncode=0)\n\n            # Installation should succeed using the second index\n            assert True  # Placeholder\n\n    def test_install_with_conflicting_packages(self, tmp_path: Path) -> None:\n        \"\"\"Test handling of conflicting packages across indices.\"\"\"\n        project_dir = tmp_path / \"test_project\"\n        project_dir.mkdir()\n\n        requirements_file = project_dir / \"requirements.yaml\"\n        requirements_file.write_text(\n            dedent(\n                \"\"\"\n                name: test_project\n                pip_indices:\n                  - https://index1.com/simple/  # Has package-a v1.0\n                  - https://index2.com/simple/  # Has package-a v2.0\n                dependencies:\n                  - pip: package-a  # Which version gets installed?\n                \"\"\",\n            ),\n        )\n\n        # First index should take precedence\n        with patch(\"subprocess.run\") as mock_run:\n            mock_run.return_value = MagicMock(returncode=0)\n\n            # Verify that first index is primary\n            assert True  # Placeholder\n\n    def test_merge_with_circular_dependencies(self, tmp_path: Path) -> None:\n        \"\"\"Test handling of circular local dependencies with pip_indices.\"\"\"\n        project_dir = tmp_path / \"test_project\"\n        project_dir.mkdir()\n\n        # Project A depends on B\n        proj_a = project_dir / \"project_a\"\n        proj_a.mkdir()\n        (proj_a / \"requirements.yaml\").write_text(\n            dedent(\n                \"\"\"\n                name: project_a\n                pip_indices:\n                  - https://pypi.org/simple/\n                local_dependencies:\n                  - ../project_b\n                dependencies:\n                  - pip: package-a\n                \"\"\",\n            ),\n        )\n\n        # Project B depends on A (circular)\n        proj_b = project_dir / \"project_b\"\n        proj_b.mkdir()\n        (proj_b / \"requirements.yaml\").write_text(\n            dedent(\n                \"\"\"\n                name: project_b\n                pip_indices:\n                  - https://custom.pypi.org/simple/\n                local_dependencies:\n                  - ../project_a\n                dependencies:\n                  - pip: package-b\n                \"\"\",\n            ),\n        )\n\n        # Should handle circular dependencies gracefully\n        # pip_indices should be merged without infinite loop\n        with patch(\"unidep._dependencies_parsing.parse_requirements\"):\n            # Implementation should detect and break circular dependencies\n            assert True  # Placeholder\n\n\nclass TestCompatibility:\n    \"\"\"Test compatibility with existing unidep features.\"\"\"\n\n    def test_pip_indices_with_platforms(self, tmp_path: Path) -> None:\n        \"\"\"Test that pip_indices work with platform selectors.\"\"\"\n        project_dir = tmp_path / \"test_project\"\n        project_dir.mkdir()\n\n        requirements_file = project_dir / \"requirements.yaml\"\n        requirements_file.write_text(\n            dedent(\n                \"\"\"\n                name: test_project\n                channels:\n                  - conda-forge\n                pip_indices:\n                  - https://pypi.org/simple/\n                platforms:\n                  - linux-64\n                  - osx-arm64\n                dependencies:\n                  - numpy  # [linux64]\n                  - pip: tensorflow  # [linux64]\n                  - pip: tensorflow-metal  # [osx-arm64]\n                \"\"\",\n            ),\n        )\n\n        # pip_indices should apply to all platforms\n        with patch(\"subprocess.run\") as mock_run:\n            mock_run.return_value = MagicMock(returncode=0)\n\n            # Verify platform-specific handling\n            assert True  # Placeholder\n\n    def test_pip_indices_with_optional_dependencies(self, tmp_path: Path) -> None:\n        \"\"\"Test that pip_indices work with optional dependencies.\"\"\"\n        project_dir = tmp_path / \"test_project\"\n        project_dir.mkdir()\n\n        requirements_file = project_dir / \"requirements.yaml\"\n        requirements_file.write_text(\n            dedent(\n                \"\"\"\n                name: test_project\n                pip_indices:\n                  - https://pypi.org/simple/\n                  - https://test.pypi.org/simple/\n                dependencies:\n                  - numpy\n                optional_dependencies:\n                  test:\n                    - pip: pytest\n                    - pip: test-package  # From test.pypi.org\n                  dev:\n                    - pip: black\n                    - pip: mypy\n                \"\"\",\n            ),\n        )\n\n        # pip_indices should apply to optional dependencies too\n        with patch(\"subprocess.run\") as mock_run:\n            mock_run.return_value = MagicMock(returncode=0)\n\n            # When installing with [test], should use pip_indices\n            assert True  # Placeholder\n\n    def test_coexistence_with_uv_index_config(self, tmp_path: Path) -> None:\n        \"\"\"Test that pip_indices can coexist with [[tool.uv.index]] config.\"\"\"\n        project_dir = tmp_path / \"test_project\"\n        project_dir.mkdir()\n\n        pyproject_file = project_dir / \"pyproject.toml\"\n        pyproject_file.write_text(\n            dedent(\n                \"\"\"\n                [tool.unidep]\n                pip_indices = [\n                    \"https://pypi.org/simple/\",\n                    \"https://unidep.index.com/simple/\"\n                ]\n                dependencies = [\"numpy\"]\n\n                [[tool.uv.index]]\n                url = \"https://uv.specific.com/simple/\"\n                name = \"uv-index\"\n                \"\"\",\n            ),\n        )\n\n        # Both configurations should be respected\n        # unidep should use pip_indices\n        # uv might use its own config when called directly\n        with patch(\"subprocess.run\") as mock_run:\n            mock_run.return_value = MagicMock(returncode=0)\n\n            # Verify both configs can coexist\n            assert True  # Placeholder\n"
  },
  {
    "path": "tests/test_pixi.py",
    "content": "\"\"\"Tests for simple Pixi.toml generation.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport textwrap\nfrom itertools import permutations\nfrom pathlib import Path\nfrom typing import Any\n\nimport pytest\n\ntry:\n    import tomllib\nexcept ImportError:  # pragma: no cover\n    import tomli as tomllib\n\nfrom unidep._conflicts import VersionConflictError\nfrom unidep._dependencies_parsing import DependencyEntry, DependencyOrigin\nfrom unidep._pixi import (\n    _add_single_file_optional_environments,\n    _collect_transitive_nodes,\n    _derive_feature_names,\n    _discover_local_dependency_graph,\n    _editable_dependency_path,\n    _extract_dependencies,\n    _feature_platforms_for_entries,\n    _filter_targets_by_platforms,\n    _make_pip_version_spec,\n    _parse_direct_requirements_for_node,\n    _parse_version_build,\n    _unique_env_name,\n    _unique_optional_feature_name,\n    _with_unique_order_paths,\n    generate_pixi_toml,\n)\nfrom unidep.platform_definitions import Spec\nfrom unidep.utils import PathWithExtras\n\n_UNSET = object()\n\n\ndef _write_file(path: Path, content: str) -> Path:\n    path.write_text(textwrap.dedent(content))\n    return path\n\n\ndef _generate_and_load(\n    output_file: Path,\n    *requirements_files: Path,\n    **kwargs: Any,\n) -> dict[str, Any]:\n    if \"verbose\" not in kwargs:\n        kwargs[\"verbose\"] = False\n    generate_pixi_toml(*requirements_files, output_file=output_file, **kwargs)\n    with output_file.open(\"rb\") as f:\n        return tomllib.load(f)\n\n\ndef _setup_app_lib_other(\n    tmp_path: Path,\n    app_optional_deps: str,\n) -> tuple[Path, Path]:\n    \"\"\"Create app/lib/other monorepo layout and return (app_req, other_req).\"\"\"\n    app_dir = tmp_path / \"app\"\n    app_dir.mkdir()\n    deps_block = textwrap.indent(textwrap.dedent(app_optional_deps), \"    \")\n    yaml_content = (\n        \"channels:\\n\"\n        \"  - conda-forge\\n\"\n        \"dependencies:\\n\"\n        \"  - pandas\\n\"\n        \"optional_dependencies:\\n\"\n        \"  dev:\\n\"\n        f\"{deps_block}\"\n    )\n    app_req = app_dir / \"requirements.yaml\"\n    app_req.write_text(yaml_content)\n\n    lib_dir = tmp_path / \"lib\"\n    lib_dir.mkdir()\n    _write_file(\n        lib_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        \"\"\",\n    )\n\n    other_dir = tmp_path / \"other\"\n    other_dir.mkdir()\n    other_req = _write_file(\n        other_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - scipy\n        \"\"\",\n    )\n\n    return app_req, other_req\n\n\ndef test_simple_pixi_generation(tmp_path: Path) -> None:\n    \"\"\"Test basic pixi.toml generation from a single requirements.yaml.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy >=1.20\n          - pandas\n          - pip: requests\n        platforms:\n          - linux-64\n          - osx-arm64\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(\n        req_file,\n        project_name=\"test-project\",\n        output_file=output_file,\n        verbose=False,\n    )\n\n    assert output_file.exists()\n    content = output_file.read_text()\n\n    # Check basic structure\n    assert \"[workspace]\" in content\n    assert 'name = \"test-project\"' in content\n    assert \"conda-forge\" in content\n    assert \"linux-64\" in content\n    assert \"osx-arm64\" in content\n\n    # Check dependencies\n    assert \"[dependencies]\" in content\n    assert 'numpy = \">=1.20\"' in content\n    assert 'pandas = \"*\"' in content\n\n    assert \"[pypi-dependencies]\" in content\n    assert 'requests = \"*\"' in content\n\n\ndef test_channels_resolution_behaviors(tmp_path: Path) -> None:\n    \"\"\"Explicit channels override file/default channels, while None falls back.\"\"\"\n    cases: list[tuple[str, str, object, list[str]]] = [\n        (\n            \"override\",\n            \"\"\"\\\n            channels:\n              - conda-forge\n            dependencies:\n              - numpy\n            platforms:\n              - linux-64\n            \"\"\",\n            [\"defaults\", \"bioconda\"],\n            [\"defaults\", \"bioconda\"],\n        ),\n        (\n            \"fallback\",\n            \"\"\"\\\n            dependencies:\n              - numpy\n            platforms:\n              - linux-64\n            \"\"\",\n            _UNSET,\n            [\"conda-forge\"],\n        ),\n        (\n            \"empty-explicit\",\n            \"\"\"\\\n            channels:\n              - conda-forge\n            dependencies:\n              - numpy\n            platforms:\n              - linux-64\n            \"\"\",\n            [],\n            [],\n        ),\n    ]\n\n    for case_name, req_content, channels_arg, expected in cases:\n        case_dir = tmp_path / case_name\n        case_dir.mkdir()\n        req_file = _write_file(case_dir / \"requirements.yaml\", req_content)\n        output_file = case_dir / \"pixi.toml\"\n        kwargs: dict[str, Any] = {}\n        if channels_arg is not _UNSET:\n            kwargs[\"channels\"] = channels_arg\n\n        data = _generate_and_load(output_file, req_file, **kwargs)\n        assert data[\"workspace\"][\"channels\"] == expected\n\n\ndef test_monorepo_pixi_generation(tmp_path: Path) -> None:\n    \"\"\"Test pixi.toml generation with features for multiple requirements files.\"\"\"\n    project1_dir = tmp_path / \"project1\"\n    project1_dir.mkdir()\n    req1 = _write_file(\n        project1_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n          - conda: scipy\n        \"\"\",\n    )\n\n    project2_dir = tmp_path / \"project2\"\n    project2_dir.mkdir()\n    req2 = _write_file(\n        project2_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n          - pip: requests\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(\n        req1,\n        req2,\n        project_name=\"monorepo\",\n        output_file=output_file,\n        verbose=False,\n    )\n\n    assert output_file.exists()\n    content = output_file.read_text()\n\n    # Check project section\n    assert \"[workspace]\" in content\n    assert 'name = \"monorepo\"' in content\n\n    # Check feature dependencies (TOML writes them directly without parent section)\n    assert \"[feature.project1.dependencies]\" in content\n    assert 'numpy = \"*\"' in content\n    assert 'scipy = \"*\"' in content\n\n    assert \"[feature.project2.dependencies]\" in content\n    assert 'pandas = \"*\"' in content\n\n    assert \"[feature.project2.pypi-dependencies]\" in content\n    assert 'requests = \"*\"' in content\n\n    # Check environments (be flexible with TOML formatting)\n    assert \"[environments]\" in content\n    assert \"default =\" in content\n    assert \"project1\" in content\n    assert \"project2\" in content\n    # Verify that default includes both projects\n    assert content.count('\"project1\"') >= 1\n    assert content.count('\"project2\"') >= 1\n\n\ndef test_pixi_monorepo_feature_names_unique_for_same_leaf_dir(tmp_path: Path) -> None:\n    \"\"\"Feature names should not collide when leaf directory names are identical.\"\"\"\n    apps_api_dir = tmp_path / \"apps\" / \"api\"\n    apps_api_dir.mkdir(parents=True)\n    apps_req = _write_file(\n        apps_api_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        \"\"\",\n    )\n\n    libs_api_dir = tmp_path / \"libs\" / \"api\"\n    libs_api_dir.mkdir(parents=True)\n    libs_req = _write_file(\n        libs_api_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", apps_req, libs_req)\n\n    features = data[\"feature\"]\n    assert len(features) == 2\n    assert len(set(features)) == 2\n\n    numpy_features = [\n        name\n        for name, feature in features.items()\n        if feature.get(\"dependencies\", {}).get(\"numpy\") == \"*\"\n    ]\n    pandas_features = [\n        name\n        for name, feature in features.items()\n        if feature.get(\"dependencies\", {}).get(\"pandas\") == \"*\"\n    ]\n    assert len(numpy_features) == 1\n    assert len(pandas_features) == 1\n    assert numpy_features[0] != pandas_features[0]\n    assert set(data[\"environments\"][\"default\"]) == {\n        numpy_features[0],\n        pandas_features[0],\n    }\n\n\ndef test_pixi_monorepo_feature_name_not_empty_for_relative_root_file(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Relative root-level requirements file should not produce an empty feature key.\"\"\"\n    root_req = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        \"\"\",\n    )\n\n    sub_dir = tmp_path / \"project\"\n    sub_dir.mkdir()\n    sub_req = _write_file(\n        sub_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        \"\"\",\n    )\n\n    monkeypatch.chdir(tmp_path)  # type: ignore[attr-defined]\n    output_file = tmp_path / \"pixi.toml\"\n    data = _generate_and_load(\n        output_file,\n        root_req.relative_to(tmp_path),\n        sub_req.relative_to(tmp_path),\n    )\n\n    features = data[\"feature\"]\n    assert len(features) == 2\n    assert \"\" not in features\n    assert all(name for name in features)\n\n\ndef test_pixi_with_version_pins(tmp_path: Path) -> None:\n    \"\"\"Test that version pins are passed through without resolution.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy >=1.20,<2.0\n          - conda: scipy =1.9.0\n          - pip: requests >2.20\n          - sympy >= 1.11\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(\n        req_file,\n        output_file=output_file,\n        verbose=False,\n    )\n\n    content = output_file.read_text()\n\n    # Check that pins are preserved exactly (spaces removed)\n    assert 'numpy = \">=1.20,<2.0\"' in content\n    assert 'scipy = \"=1.9.0\"' in content\n    assert 'requests = \">2.20\"' in content\n    assert 'sympy = \">=1.11\"' in content  # Space should be removed\n\n\ndef test_pixi_normalizes_single_equals_for_pip_pins(tmp_path: Path) -> None:\n    \"\"\"Pip pins with single '=' should be normalized to '=='.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pip: pygsti =0.9.13.3\n        \"\"\",\n    )\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        req_file,\n    )\n\n    assert data[\"pypi-dependencies\"][\"pygsti\"] == \"==0.9.13.3\"\n\n\ndef test_pixi_prefers_pip_pin_over_unpinned_conda(tmp_path: Path) -> None:\n    \"\"\"Pinned pip spec should override unpinned conda spec.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n          - pip: foo >=1.2\n            conda: foo\n        \"\"\",\n    )\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        req_file,\n    )\n\n    assert data[\"dependencies\"].get(\"foo\") is None\n    assert data[\"pypi-dependencies\"][\"foo\"] == \">=1.2\"\n\n\ndef test_pixi_prefers_conda_for_unpinned_both_sources(tmp_path: Path) -> None:\n    \"\"\"Unpinned dependencies available in both sources should use conda only.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n          - pandas\n        \"\"\",\n    )\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        req_file,\n    )\n\n    deps = data[\"dependencies\"]\n    assert deps[\"numpy\"] == \"*\"\n    assert deps[\"pandas\"] == \"*\"\n    assert \"pypi-dependencies\" not in data\n\n\ndef test_pixi_prefers_conda_for_equally_pinned_both_sources(tmp_path: Path) -> None:\n    \"\"\"When conda and pip have the same pin, use conda only.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - scipy >=1.10\n        \"\"\",\n    )\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        req_file,\n    )\n\n    assert data[\"dependencies\"][\"scipy\"] == \">=1.10\"\n    assert \"pypi-dependencies\" not in data\n\n\n# --- Parametrized single-platform conflict resolution tests ---\n\n\n@pytest.mark.parametrize(\n    (\n        \"deps_yaml\",\n        \"in_universal\",\n        \"in_universal_pypi\",\n        \"in_target_deps\",\n        \"in_target_pypi\",\n    ),\n    [\n        pytest.param(\n            \"\"\"\\\n            - click\n            - pip: click ==0.1 # [linux64]\n            \"\"\",\n            None,\n            \"==0.1\",\n            None,\n            None,\n            id=\"universal-conda-target-pip\",\n        ),\n        pytest.param(\n            \"\"\"\\\n            - conda: click >=8\n            - pip: click ==0.1 # [linux64]\n            \"\"\",\n            None,\n            \"==0.1\",\n            None,\n            None,\n            id=\"universal-pinned-conda-target-pinned-pip-prefers-target\",\n        ),\n        pytest.param(\n            \"\"\"\\\n            - conda: click >=8\n            - pip: click # [linux64]\n            \"\"\",\n            \">=8\",\n            None,\n            None,\n            None,\n            id=\"universal-pinned-conda-beats-target-unpinned-pip\",\n        ),\n        pytest.param(\n            \"\"\"\\\n            - pip: click\n            - conda: click >=8 # [linux64]\n            \"\"\",\n            \">=8\",\n            None,\n            None,\n            None,\n            id=\"universal-pip-target-conda-prefers-narrower-conda\",\n        ),\n        pytest.param(\n            \"\"\"\\\n            - pip: click ==0.1\n            - conda: click # [linux64]\n            \"\"\",\n            None,\n            \"==0.1\",\n            None,\n            None,\n            id=\"universal-pinned-pip-beats-target-unpinned-conda\",\n        ),\n    ],\n)\ndef test_pixi_reconciles_single_platform_conflict(\n    tmp_path: Path,\n    deps_yaml: str,\n    in_universal: str | None,\n    in_universal_pypi: str | None,\n    in_target_deps: str | None,\n    in_target_pypi: str | None,\n) -> None:\n    \"\"\"Single-platform pixi output compresses the winner into the universal section.\"\"\"\n    deps_block = textwrap.indent(textwrap.dedent(deps_yaml), \"  \")\n    yaml_content = (\n        \"channels:\\n\"\n        \"  - conda-forge\\n\"\n        \"dependencies:\\n\"\n        f\"{deps_block}\"\n        \"platforms:\\n\"\n        \"  - linux-64\\n\"\n    )\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(yaml_content)\n    data = _generate_and_load(tmp_path / \"pixi.toml\", req_file)\n\n    if in_universal is not None:\n        assert data[\"dependencies\"][\"click\"] == in_universal\n    else:\n        assert \"click\" not in data.get(\"dependencies\", {})\n\n    if in_universal_pypi is not None:\n        assert data[\"pypi-dependencies\"][\"click\"] == in_universal_pypi\n    else:\n        assert \"click\" not in data.get(\"pypi-dependencies\", {})\n\n    linux_target = data.get(\"target\", {}).get(\"linux-64\", {})\n    if in_target_deps is not None:\n        assert linux_target[\"dependencies\"][\"click\"] == in_target_deps\n    else:\n        assert \"click\" not in linux_target.get(\"dependencies\", {})\n\n    if in_target_pypi is not None:\n        assert linux_target[\"pypi-dependencies\"][\"click\"] == in_target_pypi\n    else:\n        assert \"click\" not in linux_target.get(\"pypi-dependencies\", {})\n\n\ndef test_pixi_reconcile_is_order_independent_for_universal_and_target_conflicts(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Universal/target conflict reconciliation should not depend on declaration order.\"\"\"\n    req_target_then_universal = _write_file(\n        tmp_path / \"target_then_universal.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pip: click ==0.1 # [linux64]\n          - conda: click >=8\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    req_universal_then_target = _write_file(\n        tmp_path / \"universal_then_target.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - conda: click >=8\n          - pip: click ==0.1 # [linux64]\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    out1 = tmp_path / \"pixi-target-then-universal.toml\"\n    out2 = tmp_path / \"pixi-universal-then-target.toml\"\n    data1 = _generate_and_load(out1, req_target_then_universal)\n    data2 = _generate_and_load(out2, req_universal_then_target)\n\n    assert data1 == data2\n    assert \"click\" not in data1.get(\"dependencies\", {})\n    assert data1[\"pypi-dependencies\"][\"click\"] == \"==0.1\"\n    assert \"target\" not in data1\n\n\ndef test_pixi_demoted_reconciliation_is_order_independent_with_repeated_universals(\n    tmp_path: Path,\n) -> None:\n    \"\"\"All declaration orders should yield the same reconciled demoted result.\"\"\"\n    deps = [\n        \"- conda: click >=8\",\n        \"- pip: click ==0.1 # [linux64]\",\n        \"- conda: click >=9\",\n    ]\n\n    results = []\n    for i, dep_order in enumerate(permutations(deps)):\n        deps_block = \"\\n\".join(f\"  {dep}\" for dep in dep_order)\n        req_file = _write_file(\n            tmp_path / f\"requirements-{i}.yaml\",\n            (\n                \"channels:\\n\"\n                \"  - conda-forge\\n\"\n                \"dependencies:\\n\"\n                f\"{deps_block}\\n\"\n                \"platforms:\\n\"\n                \"  - linux-64\\n\"\n                \"  - osx-64\\n\"\n            ),\n        )\n        data = _generate_and_load(tmp_path / f\"pixi-{i}.toml\", req_file)\n\n        assert data[\"target\"][\"linux-64\"][\"pypi-dependencies\"][\"click\"] == \"==0.1\"\n        assert data[\"target\"][\"osx-64\"][\"dependencies\"][\"click\"] == \">=9\"\n        assert \"click\" not in data.get(\"dependencies\", {})\n        assert \"click\" not in data.get(\"pypi-dependencies\", {})\n        results.append(data)\n\n    assert all(result == results[0] for result in results[1:])\n\n\n# --- Parametrized multiplatform conflict resolution tests ---\n\n\n@pytest.mark.parametrize(\n    (\"deps_yaml\", \"linux_section\", \"linux_val\", \"osx_section\", \"osx_val\"),\n    [\n        pytest.param(\n            \"\"\"\\\n            - conda: click >=8\n            - pip: click ==0.1 # [linux64]\n            \"\"\",\n            \"pypi-dependencies\",\n            \"==0.1\",\n            \"dependencies\",\n            \">=8\",\n            id=\"universal-conda-target-pip-multiplatform\",\n        ),\n        pytest.param(\n            \"\"\"\\\n            - pip: click ==0.1\n            - conda: click >=8 # [linux64]\n            \"\"\",\n            \"dependencies\",\n            \">=8\",\n            \"pypi-dependencies\",\n            \"==0.1\",\n            id=\"universal-pip-target-conda-multiplatform\",\n        ),\n    ],\n)\ndef test_pixi_reconciles_multiplatform_conflict(\n    tmp_path: Path,\n    deps_yaml: str,\n    linux_section: str,\n    linux_val: str,\n    osx_section: str,\n    osx_val: str,\n) -> None:\n    \"\"\"Universal deps should be promoted to non-overriding target platforms.\"\"\"\n    deps_block = textwrap.indent(textwrap.dedent(deps_yaml), \"  \")\n    yaml_content = (\n        \"channels:\\n\"\n        \"  - conda-forge\\n\"\n        \"dependencies:\\n\"\n        f\"{deps_block}\"\n        \"platforms:\\n\"\n        \"  - linux-64\\n\"\n        \"  - osx-arm64\\n\"\n    )\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(yaml_content)\n    data = _generate_and_load(tmp_path / \"pixi.toml\", req_file)\n\n    assert \"click\" not in data.get(\"dependencies\", {})\n    assert \"click\" not in data.get(\"pypi-dependencies\", {})\n    assert data[\"target\"][\"linux-64\"][linux_section][\"click\"] == linux_val\n    assert data[\"target\"][\"osx-arm64\"][osx_section][\"click\"] == osx_val\n\n\ndef test_pixi_with_local_package(tmp_path: Path) -> None:\n    \"\"\"Test that local packages are added as editable dependencies.\"\"\"\n    project_dir = tmp_path / \"my_package\"\n    project_dir.mkdir()\n\n    _write_file(\n        project_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        \"\"\",\n    )\n\n    _write_file(\n        project_dir / \"pyproject.toml\",\n        \"\"\"\\\n        [build-system]\n        requires = [\"setuptools\"]\n\n        [project]\n        name = \"my-package\"\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(\n        project_dir,\n        output_file=output_file,\n        verbose=False,\n    )\n\n    assert output_file.exists()\n    content = output_file.read_text()\n\n    assert \"pypi-dependencies\" in content\n    assert \"my_package\" in content\n    assert 'path = \"./my_package\"' in content\n    assert \"editable = true\" in content\n    assert 'numpy = \"*\"' in content\n\n\ndef test_pixi_single_file_editable_path_relative_to_output(tmp_path: Path) -> None:\n    \"\"\"Single-file mode should use editable path relative to output location.\"\"\"\n    project_dir = tmp_path / \"services\" / \"api\"\n    project_dir.mkdir(parents=True)\n\n    _write_file(\n        project_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        \"\"\",\n    )\n\n    _write_file(\n        project_dir / \"pyproject.toml\",\n        \"\"\"\\\n        [build-system]\n        requires = [\"setuptools\"]\n\n        [project]\n        name = \"service-api\"\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", project_dir / \"requirements.yaml\")\n\n    editable_dep = data[\"pypi-dependencies\"][\"service_api\"]\n    assert editable_dep[\"editable\"] is True\n    assert editable_dep[\"path\"] == \"./services/api\"\n\n\ndef test_pixi_single_file_includes_local_dependency_package_as_editable(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Single-file mode should install local dependency projects as editable packages.\"\"\"\n    app_dir = tmp_path / \"app\"\n    app_dir.mkdir()\n    req_file = _write_file(\n        app_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        local_dependencies:\n          - ../lib\n        \"\"\",\n    )\n\n    lib_dir = tmp_path / \"lib\"\n    lib_dir.mkdir()\n    _write_file(\n        lib_dir / \"requirements.yaml\",\n        \"\"\"\\\n        dependencies:\n          - pandas\n        \"\"\",\n    )\n    _write_file(\n        lib_dir / \"pyproject.toml\",\n        \"\"\"\\\n        [build-system]\n        requires = [\"setuptools\"]\n\n        [project]\n        name = \"lib\"\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", req_file)\n\n    assert data[\"dependencies\"][\"numpy\"] == \"*\"\n    assert data[\"dependencies\"][\"pandas\"] == \"*\"\n    lib_editable = data[\"pypi-dependencies\"][\"lib\"]\n    assert lib_editable[\"editable\"] is True\n    assert lib_editable[\"path\"] == \"./lib\"\n\n\ndef test_pixi_empty_dependencies(tmp_path: Path) -> None:\n    \"\"\"Test handling of requirements file with no dependencies.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(\n        req_file,\n        output_file=output_file,\n        verbose=False,\n    )\n\n    assert output_file.exists()\n    content = output_file.read_text()\n\n    assert \"[workspace]\" in content\n    assert \"[dependencies]\" not in content\n    assert \"[pypi-dependencies]\" not in content\n\n\ndef test_pixi_with_platform_selectors(tmp_path: Path) -> None:\n    \"\"\"Test that platform selectors are converted to target sections.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n          - cuda-toolkit =11.8  # [linux64]\n          - pip: pyobjc  # [osx]\n        platforms:\n          - linux-64\n          - osx-arm64\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        req_file,\n        project_name=\"test-selectors\",\n    )\n\n    assert data[\"dependencies\"][\"numpy\"] == \"*\"\n    assert \"cuda-toolkit\" not in data[\"dependencies\"]\n    assert \"pyobjc\" not in data.get(\"pypi-dependencies\", {})\n\n    assert data[\"target\"][\"linux-64\"][\"dependencies\"][\"cuda-toolkit\"] == \"=11.8\"\n    osx_target = data[\"target\"].get(\"osx-arm64\") or data[\"target\"].get(\"osx-64\")\n    assert osx_target is not None\n    assert osx_target[\"pypi-dependencies\"][\"pyobjc\"] == \"*\"\n\n\ndef test_pixi_selector_targets_preserved_without_explicit_platforms(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Selector targets should not be dropped when input files omit platforms.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n          - cuda-toolkit  # [linux64]\n          - pip: pyobjc  # [osx]\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", req_file)\n\n    assert \"linux-64\" in data[\"workspace\"][\"platforms\"]\n    assert any(p in data[\"workspace\"][\"platforms\"] for p in (\"osx-64\", \"osx-arm64\"))\n    assert data[\"target\"][\"linux-64\"][\"dependencies\"][\"cuda-toolkit\"] == \"*\"\n    osx_target = data[\"target\"].get(\"osx-arm64\") or data[\"target\"].get(\"osx-64\")\n    assert osx_target is not None\n    assert osx_target[\"pypi-dependencies\"][\"pyobjc\"] == \"*\"\n\n\ndef test_pixi_with_multiple_platform_selectors(tmp_path: Path) -> None:\n    \"\"\"Test that broad selectors like 'unix' expand to multiple platforms.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n          - readline  # [unix]\n          - pywin32  # [win64]\n        platforms:\n          - linux-64\n          - osx-arm64\n          - win-64\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        req_file,\n        project_name=\"test-multi-platform\",\n    )\n\n    assert data[\"dependencies\"][\"numpy\"] == \"*\"\n    assert \"readline\" not in data[\"dependencies\"]\n    assert \"pywin32\" not in data[\"dependencies\"]\n    assert data[\"target\"][\"linux-64\"][\"dependencies\"][\"readline\"] == \"*\"\n    assert data[\"target\"][\"osx-arm64\"][\"dependencies\"][\"readline\"] == \"*\"\n    assert data[\"target\"][\"win-64\"][\"dependencies\"][\"pywin32\"] == \"*\"\n\n\ndef test_pixi_monorepo_with_platform_selectors(tmp_path: Path) -> None:\n    \"\"\"Test platform selectors in monorepo mode (multiple files).\"\"\"\n    project1_dir = tmp_path / \"project1\"\n    project1_dir.mkdir()\n    req1 = _write_file(\n        project1_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n          - cuda-toolkit  # [linux64]\n        platforms:\n          - linux-64\n          - osx-arm64\n        \"\"\",\n    )\n\n    project2_dir = tmp_path / \"project2\"\n    project2_dir.mkdir()\n    req2 = _write_file(\n        project2_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n          - pip: pyobjc  # [arm64]\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        req1,\n        req2,\n        project_name=\"monorepo-selectors\",\n    )\n\n    project1 = data[\"feature\"][\"project1\"]\n    project2 = data[\"feature\"][\"project2\"]\n\n    assert \"dependencies\" not in project1\n    assert project1[\"target\"][\"linux-64\"][\"dependencies\"][\"numpy\"] == \"*\"\n    assert project1[\"target\"][\"osx-arm64\"][\"dependencies\"][\"numpy\"] == \"*\"\n    assert project1[\"target\"][\"linux-64\"][\"dependencies\"][\"cuda-toolkit\"] == \"*\"\n\n    assert project2[\"dependencies\"][\"pandas\"] == \"*\"\n    assert \"pyobjc\" not in project2.get(\"pypi-dependencies\", {})\n    assert project2[\"target\"][\"osx-arm64\"][\"pypi-dependencies\"][\"pyobjc\"] == \"*\"\n\n\ndef test_pixi_monorepo_preserves_selector_only_platforms_without_declared_platforms(\n    tmp_path: Path,\n) -> None:\n    project1_dir = tmp_path / \"project1\"\n    project1_dir.mkdir()\n    req1 = _write_file(\n        project1_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        platforms:\n          - linux-64\n        dependencies:\n          - numpy\n        \"\"\",\n    )\n\n    project2_dir = tmp_path / \"project2\"\n    project2_dir.mkdir()\n    req2 = _write_file(\n        project2_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pip: pyobjc  # [osx]\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        req1,\n        req2,\n        project_name=\"monorepo-selector-only-platforms\",\n    )\n\n    assert data[\"workspace\"][\"platforms\"] == [\"linux-64\", \"osx-64\", \"osx-arm64\"]\n    project2 = data[\"feature\"][\"project2\"]\n    assert \"pypi-dependencies\" not in project2\n    assert project2[\"target\"][\"osx-64\"][\"pypi-dependencies\"][\"pyobjc\"] == \"*\"\n    assert project2[\"target\"][\"osx-arm64\"][\"pypi-dependencies\"][\"pyobjc\"] == \"*\"\n\n\ndef test_pixi_single_file_preserves_selector_only_platforms_without_declared_platforms(\n    tmp_path: Path,\n) -> None:\n    req = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy  # [linux]\n          - numpy  # [osx]\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        req,\n        project_name=\"single-file-selector-only-platforms\",\n    )\n\n    assert data[\"workspace\"][\"platforms\"] == [\n        \"linux-64\",\n        \"linux-aarch64\",\n        \"linux-ppc64le\",\n        \"osx-64\",\n        \"osx-arm64\",\n    ]\n    assert data[\"dependencies\"][\"numpy\"] == \"*\"\n    assert \"target\" not in data\n\n\ndef test_pixi_monorepo_optional_group_preserves_selector_only_platforms(\n    tmp_path: Path,\n) -> None:\n    project1_dir = tmp_path / \"project1\"\n    project1_dir.mkdir()\n    req1 = _write_file(\n        project1_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        platforms:\n          - linux-64\n        dependencies:\n          - numpy\n        \"\"\",\n    )\n\n    project2_dir = tmp_path / \"project2\"\n    project2_dir.mkdir()\n    req2 = _write_file(\n        project2_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        optional_dependencies:\n          dev:\n            - pip: pyobjc  # [osx]\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        req1,\n        req2,\n        project_name=\"monorepo-selector-only-optional-platforms\",\n    )\n\n    assert data[\"workspace\"][\"platforms\"] == [\"linux-64\", \"osx-64\", \"osx-arm64\"]\n    project2_dev = data[\"feature\"][\"project2-dev\"]\n    assert \"pypi-dependencies\" not in project2_dev\n    assert project2_dev[\"target\"][\"osx-64\"][\"pypi-dependencies\"][\"pyobjc\"] == \"*\"\n    assert project2_dev[\"target\"][\"osx-arm64\"][\"pypi-dependencies\"][\"pyobjc\"] == \"*\"\n\n\ndef test_pixi_single_file_optional_group_preserves_selector_only_platforms(\n    tmp_path: Path,\n) -> None:\n    req = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        optional_dependencies:\n          dev:\n            - numpy  # [linux]\n            - numpy  # [osx]\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        req,\n        project_name=\"single-file-selector-only-optional-platforms\",\n    )\n\n    assert data[\"workspace\"][\"platforms\"] == [\n        \"linux-64\",\n        \"linux-aarch64\",\n        \"linux-ppc64le\",\n        \"osx-64\",\n        \"osx-arm64\",\n    ]\n    assert data[\"feature\"][\"dev\"][\"dependencies\"][\"numpy\"] == \"*\"\n\n\ndef test_pixi_single_file_optional_group_keeps_platform_specific_dep_targeted(\n    tmp_path: Path,\n) -> None:\n    req = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - click  # [linux]\n        optional_dependencies:\n          dev:\n            - pip: pyobjc  # [osx]\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        req,\n        project_name=\"single-file-optional-platform-hoist\",\n    )\n\n    assert data[\"workspace\"][\"platforms\"] == [\n        \"linux-64\",\n        \"linux-aarch64\",\n        \"linux-ppc64le\",\n        \"osx-64\",\n        \"osx-arm64\",\n    ]\n    dev = data[\"feature\"][\"dev\"]\n    assert \"pypi-dependencies\" not in dev\n    assert dev[\"target\"][\"osx-64\"][\"pypi-dependencies\"][\"pyobjc\"] == \"*\"\n    assert dev[\"target\"][\"osx-arm64\"][\"pypi-dependencies\"][\"pyobjc\"] == \"*\"\n\n\n@pytest.mark.parametrize(\n    (\"first_pin\", \"second_pin\"),\n    [\n        (\">1\", \"<1\"),\n        (\"~=1.0\", \"<1\"),\n        (\"==1\", \"!=1\"),\n    ],\n)\ndef test_pixi_rejects_contradictory_pip_constraints(\n    tmp_path: Path,\n    first_pin: str,\n    second_pin: str,\n) -> None:\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        f\"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pip: pkg {first_pin}\n          - pip: pkg {second_pin}\n        \"\"\",\n    )\n\n    with pytest.raises(VersionConflictError, match=\"pkg\"):\n        generate_pixi_toml(req_file, output_file=tmp_path / \"pixi.toml\", verbose=False)\n\n\ndef test_pixi_monorepo_with_local_packages(tmp_path: Path) -> None:\n    \"\"\"Test that local packages in monorepo are added as editable dependencies.\"\"\"\n    project1_dir = tmp_path / \"project1\"\n    project1_dir.mkdir()\n    req1 = _write_file(\n        project1_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        \"\"\",\n    )\n    _write_file(\n        project1_dir / \"pyproject.toml\",\n        \"\"\"\\\n        [build-system]\n        requires = [\"setuptools\"]\n\n        [project]\n        name = \"project-one\"\n        \"\"\",\n    )\n\n    project2_dir = tmp_path / \"project2\"\n    project2_dir.mkdir()\n    req2 = _write_file(\n        project2_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        \"\"\",\n    )\n    _write_file(\n        project2_dir / \"pyproject.toml\",\n        \"\"\"\\\n        [build-system]\n        requires = [\"setuptools\"]\n\n        [project]\n        name = \"project-two\"\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(\n        req1,\n        req2,\n        project_name=\"monorepo-local\",\n        output_file=output_file,\n        verbose=False,\n    )\n\n    assert output_file.exists()\n    content = output_file.read_text()\n\n    assert \"[feature.project1.pypi-dependencies.project_one]\" in content\n    assert \"[feature.project2.pypi-dependencies.project_two]\" in content\n    assert 'path = \"./project1\"' in content\n    assert 'path = \"./project2\"' in content\n    assert \"editable = true\" in content\n\n\ndef test_pixi_monorepo_keeps_unmanaged_local_dependency_as_editable(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Monorepo mode should keep unmanaged but installable local packages.\"\"\"\n    app_dir = tmp_path / \"app\"\n    app_dir.mkdir()\n    req_app = _write_file(\n        app_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        local_dependencies:\n          - ../lib\n        \"\"\",\n    )\n\n    other_dir = tmp_path / \"other\"\n    other_dir.mkdir()\n    req_other = _write_file(\n        other_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        \"\"\",\n    )\n\n    lib_dir = tmp_path / \"lib\"\n    lib_dir.mkdir()\n    _write_file(\n        lib_dir / \"pyproject.toml\",\n        \"\"\"\\\n        [build-system]\n        requires = [\"setuptools\"]\n\n        [project]\n        name = \"lib-pkg\"\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", req_app, req_other)\n\n    assert \"lib\" not in data[\"feature\"]\n    app_editable = data[\"feature\"][\"app\"][\"pypi-dependencies\"][\"lib_pkg\"]\n    assert app_editable[\"editable\"] is True\n    assert app_editable[\"path\"] == \"./lib\"\n\n\ndef test_pixi_monorepo_optional_unmanaged_deduped_against_base(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Unmanaged local dep in both base and optional should only appear in base feature.\"\"\"\n    app_dir = tmp_path / \"app\"\n    app_dir.mkdir()\n    _write_file(\n        app_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        local_dependencies:\n          - ../lib\n        optional_dependencies:\n          dev:\n            - ../lib\n        \"\"\",\n    )\n\n    lib_dir = tmp_path / \"lib\"\n    lib_dir.mkdir()\n    _write_file(\n        lib_dir / \"pyproject.toml\",\n        \"\"\"\\\n        [build-system]\n        requires = [\"setuptools\"]\n\n        [project]\n        name = \"lib-pkg\"\n        \"\"\",\n    )\n\n    other_dir = tmp_path / \"other\"\n    other_dir.mkdir()\n    _write_file(\n        other_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        app_dir / \"requirements.yaml\",\n        other_dir / \"requirements.yaml\",\n    )\n\n    assert \"lib_pkg\" in data[\"feature\"][\"app\"][\"pypi-dependencies\"]\n    opt_feature_name = \"app-dev\"\n    if opt_feature_name in data.get(\"feature\", {}):\n        opt_pypi = data[\"feature\"][opt_feature_name].get(\"pypi-dependencies\", {})\n        assert \"lib_pkg\" not in opt_pypi, (\n            \"Unmanaged local dep should be deduped from optional feature\"\n        )\n\n\ndef test_pixi_monorepo_optional_unmanaged_only_group_creates_feature(\n    tmp_path: Path,\n) -> None:\n    \"\"\"An optional group with only unmanaged local deps should still create a feature.\"\"\"\n    app_dir = tmp_path / \"app\"\n    app_dir.mkdir()\n    _write_file(\n        app_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        optional_dependencies:\n          dev:\n            - ../lib\n        \"\"\",\n    )\n\n    lib_dir = tmp_path / \"lib\"\n    lib_dir.mkdir()\n    _write_file(\n        lib_dir / \"pyproject.toml\",\n        \"\"\"\\\n        [build-system]\n        requires = [\"setuptools\"]\n\n        [project]\n        name = \"lib-pkg\"\n        \"\"\",\n    )\n\n    other_dir = tmp_path / \"other\"\n    other_dir.mkdir()\n    _write_file(\n        other_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        app_dir / \"requirements.yaml\",\n        other_dir / \"requirements.yaml\",\n    )\n\n    opt_feature_name = \"app-dev\"\n    assert opt_feature_name in data[\"feature\"], (\n        f\"Expected feature '{opt_feature_name}' for unmanaged-only optional group\"\n    )\n    opt_pypi = data[\"feature\"][opt_feature_name].get(\"pypi-dependencies\", {})\n    assert \"lib_pkg\" in opt_pypi\n    assert opt_pypi[\"lib_pkg\"][\"editable\"] is True\n\n    env_name = opt_feature_name.replace(\"_\", \"-\")\n    assert env_name in data[\"environments\"]\n    assert opt_feature_name in data[\"environments\"][env_name]\n\n\ndef test_pixi_monorepo_editable_paths_use_project_paths(tmp_path: Path) -> None:\n    \"\"\"Editable paths should point to project dirs, not derived feature names.\"\"\"\n    apps_api_dir = tmp_path / \"apps\" / \"api\"\n    apps_api_dir.mkdir(parents=True)\n    _write_file(\n        apps_api_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        \"\"\",\n    )\n    _write_file(\n        apps_api_dir / \"pyproject.toml\",\n        \"\"\"\\\n        [build-system]\n        requires = [\"setuptools\"]\n\n        [project]\n        name = \"apps-api\"\n        \"\"\",\n    )\n\n    libs_api_dir = tmp_path / \"libs\" / \"api\"\n    libs_api_dir.mkdir(parents=True)\n    _write_file(\n        libs_api_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        \"\"\",\n    )\n    _write_file(\n        libs_api_dir / \"pyproject.toml\",\n        \"\"\"\\\n        [build-system]\n        requires = [\"setuptools\"]\n\n        [project]\n        name = \"libs-api\"\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        apps_api_dir / \"requirements.yaml\",\n        libs_api_dir / \"requirements.yaml\",\n    )\n\n    editable_paths = {\n        dep_data[\"path\"]\n        for feature in data[\"feature\"].values()\n        for dep_data in feature.get(\"pypi-dependencies\", {}).values()\n        if isinstance(dep_data, dict) and dep_data.get(\"editable\") is True\n    }\n    assert editable_paths == {\"./apps/api\", \"./libs/api\"}\n\n\ndef test_pixi_monorepo_shared_local_file_becomes_single_feature(tmp_path: Path) -> None:\n    \"\"\"Shared local requirements should be represented as a separate feature.\"\"\"\n    _write_file(\n        tmp_path / \"dev-requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pytest\n        \"\"\",\n    )\n\n    project1_dir = tmp_path / \"project1\"\n    project1_dir.mkdir()\n    req1 = _write_file(\n        project1_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        local_dependencies:\n          - ../dev-requirements.yaml\n        \"\"\",\n    )\n\n    project2_dir = tmp_path / \"project2\"\n    project2_dir.mkdir()\n    req2 = _write_file(\n        project2_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        local_dependencies:\n          - ../dev-requirements.yaml\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", req1, req2)\n\n    features = data[\"feature\"]\n    project1_feature = next(\n        name\n        for name, feature in features.items()\n        if feature.get(\"dependencies\", {}).get(\"numpy\") == \"*\"\n    )\n    project2_feature = next(\n        name\n        for name, feature in features.items()\n        if feature.get(\"dependencies\", {}).get(\"pandas\") == \"*\"\n    )\n    shared_feature = next(\n        name\n        for name, feature in features.items()\n        if feature.get(\"dependencies\", {}).get(\"pytest\") == \"*\"\n    )\n\n    assert project1_feature != shared_feature\n    assert project2_feature != shared_feature\n    assert shared_feature.startswith(\"dev-requirements\")\n    assert \"pytest\" not in features[project1_feature].get(\"dependencies\", {})\n    assert \"pytest\" not in features[project2_feature].get(\"dependencies\", {})\n\n    assert set(data[\"environments\"][\"default\"]) == {\n        project1_feature,\n        project2_feature,\n        shared_feature,\n    }\n\n\ndef test_pixi_monorepo_transitive_local_dependencies_are_composed_in_envs(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Features should stay local while envs include transitive local dependencies.\"\"\"\n    project_c = tmp_path / \"project_c\"\n    project_c.mkdir()\n    _write_file(\n        project_c / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - sympy\n        \"\"\",\n    )\n\n    project_b = tmp_path / \"project_b\"\n    project_b.mkdir()\n    _write_file(\n        project_b / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        local_dependencies:\n          - ../project_c\n        \"\"\",\n    )\n\n    project_a = tmp_path / \"project_a\"\n    project_a.mkdir()\n    req_a = _write_file(\n        project_a / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        local_dependencies:\n          - ../project_b\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        req_a,\n        project_c / \"requirements.yaml\",\n    )\n\n    features = data[\"feature\"]\n    feature_a = next(\n        name\n        for name, feature in features.items()\n        if feature.get(\"dependencies\", {}).get(\"numpy\") == \"*\"\n    )\n    feature_b = next(\n        name\n        for name, feature in features.items()\n        if feature.get(\"dependencies\", {}).get(\"pandas\") == \"*\"\n    )\n    feature_c = next(\n        name\n        for name, feature in features.items()\n        if feature.get(\"dependencies\", {}).get(\"sympy\") == \"*\"\n    )\n\n    assert \"pandas\" not in features[feature_a].get(\"dependencies\", {})\n    assert \"sympy\" not in features[feature_a].get(\"dependencies\", {})\n    assert \"sympy\" not in features[feature_b].get(\"dependencies\", {})\n\n    assert set(data[\"environments\"][\"default\"]) == {feature_a, feature_b, feature_c}\n\n\ndef test_pixi_monorepo_ignores_wheel_local_dependencies_in_graph(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Multi-file mode should skip wheel/zip locals while discovering features.\"\"\"\n    wheels_dir = tmp_path / \"wheels\"\n    wheels_dir.mkdir()\n    wheel_file = wheels_dir / \"example-0.1.0-py3-none-any.whl\"\n    wheel_file.write_text(\"not-a-real-wheel\")\n\n    project1 = tmp_path / \"project1\"\n    project1.mkdir()\n    req1 = _write_file(\n        project1 / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        local_dependencies:\n          - ../wheels/example-0.1.0-py3-none-any.whl\n        \"\"\",\n    )\n\n    project2 = tmp_path / \"project2\"\n    project2.mkdir()\n    req2 = _write_file(\n        project2 / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", req1, req2)\n\n    assert set(data[\"feature\"]) == {\"project1\", \"project2\"}\n\n\ndef test_pixi_single_file_local_dependency_use_modes(tmp_path: Path) -> None:\n    \"\"\"`use: pypi` should add pip dep, while `use: skip` should add nothing.\"\"\"\n    pypi_local = tmp_path / \"pypi_local\"\n    pypi_local.mkdir()\n    _write_file(\n        pypi_local / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        \"\"\",\n    )\n\n    skipped_local = tmp_path / \"skipped_local\"\n    skipped_local.mkdir()\n    _write_file(\n        skipped_local / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - scipy\n        \"\"\",\n    )\n\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        local_dependencies:\n          - local: ./pypi_local\n            use: pypi\n            pypi: pypi-local-package >=1.2\n          - local: ./skipped_local\n            use: skip\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", req_file)\n\n    assert data[\"dependencies\"][\"numpy\"] == \"*\"\n    assert \"pandas\" not in data[\"dependencies\"]\n    assert \"scipy\" not in data[\"dependencies\"]\n    assert data[\"pypi-dependencies\"][\"pypi-local-package\"] == \">=1.2\"\n    assert \"skipped_local\" not in data.get(\"pypi-dependencies\", {})\n    assert \"target\" not in data\n\n\ndef test_pixi_with_directory_input(tmp_path: Path) -> None:\n    \"\"\"Test passing a directory instead of a file.\"\"\"\n    project_dir = tmp_path / \"myproject\"\n    project_dir.mkdir()\n    _write_file(\n        project_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(\n        project_dir,\n        output_file=output_file,\n        verbose=False,\n    )\n\n    assert output_file.exists()\n    content = output_file.read_text()\n    assert 'numpy = \"*\"' in content\n\n\ndef test_pixi_verbose_output(tmp_path: Path, capsys: object) -> None:\n    \"\"\"Test verbose output mode.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(\n        req_file,\n        output_file=output_file,\n        verbose=True,\n    )\n\n    captured = capsys.readouterr()  # type: ignore[attr-defined]\n    assert \"Generated pixi.toml\" in captured.out\n\n\ndef test_pixi_fallback_package_name(tmp_path: Path) -> None:\n    \"\"\"Test fallback to directory name when pyproject.toml has no project.name.\"\"\"\n    project_dir = tmp_path / \"my_fallback_pkg\"\n    project_dir.mkdir()\n\n    _write_file(\n        project_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        \"\"\",\n    )\n\n    _write_file(\n        project_dir / \"pyproject.toml\",\n        \"\"\"\\\n        [build-system]\n        requires = [\"setuptools\"]\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(\n        project_dir,\n        output_file=output_file,\n        verbose=False,\n    )\n\n    content = output_file.read_text()\n    assert \"my_fallback_pkg\" in content\n\n\ndef test_pixi_filtering_removes_empty_targets(tmp_path: Path) -> None:\n    \"\"\"Test that filtering removes targets entirely when no platforms match.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n          - cuda-toolkit  # [linux64]\n        platforms:\n          - osx-arm64\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(\n        req_file,\n        output_file=output_file,\n        verbose=False,\n    )\n\n    content = output_file.read_text()\n    assert \"cuda-toolkit\" not in content\n    assert \"[target.\" not in content\n\n\ndef test_pixi_stdout_output(tmp_path: Path, capsys: object) -> None:\n    \"\"\"Test output to stdout when output_file is None.\"\"\"\n    _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    generate_pixi_toml(\n        tmp_path / \"requirements.yaml\",\n        output_file=None,\n        verbose=False,\n    )\n\n    captured = capsys.readouterr()  # type: ignore[attr-defined]\n    assert 'numpy = \"*\"' in captured.out\n    assert \"[workspace]\" in captured.out\n\n\ndef test_pixi_monorepo_with_directory_input(tmp_path: Path) -> None:\n    \"\"\"Test monorepo mode passing directories instead of files.\"\"\"\n    project1_dir = tmp_path / \"proj1\"\n    project1_dir.mkdir()\n    _write_file(\n        project1_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        \"\"\",\n    )\n\n    project2_dir = tmp_path / \"proj2\"\n    project2_dir.mkdir()\n    _write_file(\n        project2_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(\n        project1_dir,\n        project2_dir,\n        project_name=\"monorepo-dirs\",\n        output_file=output_file,\n        verbose=False,\n    )\n\n    assert output_file.exists()\n    content = output_file.read_text()\n    assert \"[feature.proj1.dependencies]\" in content\n    assert \"[feature.proj2.dependencies]\" in content\n\n\ndef test_pixi_monorepo_filtering_removes_empty_feature_targets(tmp_path: Path) -> None:\n    \"\"\"Test that filtering removes empty feature targets in monorepo mode.\"\"\"\n    project1_dir = tmp_path / \"project1\"\n    project1_dir.mkdir()\n    req1 = _write_file(\n        project1_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n          - cuda-toolkit  # [linux64]\n        platforms:\n          - osx-arm64\n        \"\"\",\n    )\n\n    project2_dir = tmp_path / \"project2\"\n    project2_dir.mkdir()\n    req2 = _write_file(\n        project2_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(\n        req1,\n        req2,\n        project_name=\"monorepo-filter\",\n        output_file=output_file,\n        verbose=False,\n    )\n\n    content = output_file.read_text()\n    assert \"cuda-toolkit\" not in content\n    assert \"[feature.project1.target.osx-arm64.dependencies]\" in content\n    assert \"[feature.project1.target.linux-64.dependencies]\" not in content\n\n\ndef test_pixi_default_cwd(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:\n    \"\"\"Test that generate_pixi_toml uses cwd when no args provided.\"\"\"\n    _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    monkeypatch.chdir(tmp_path)  # type: ignore[attr-defined]\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(\n        output_file=output_file,\n        verbose=False,\n    )\n\n    assert output_file.exists()\n    content = output_file.read_text()\n    assert 'numpy = \"*\"' in content\n\n\ndef test_pixi_optional_dependencies_single_file(tmp_path: Path) -> None:\n    \"\"\"Test optional dependencies with realistic user scenario.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy >=1.20\n        optional_dependencies:\n          dev:\n            - pytest >=7.0\n            - pip: black\n            - pexpect  # [unix]\n            - wexpect  # [win64]\n          docs:\n            - sphinx\n            - sphinx-rtd-theme\n        platforms:\n          - linux-64\n          - win-64\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(\n        req_file,\n        project_name=\"test-project\",\n        output_file=output_file,\n        verbose=False,\n    )\n\n    assert output_file.exists()\n    content = output_file.read_text()\n\n    assert \"[dependencies]\" in content\n    assert 'numpy = \">=1.20\"' in content\n\n    assert \"[feature.dev.dependencies]\" in content\n    assert 'pytest = \">=7.0\"' in content\n    assert \"[feature.dev.pypi-dependencies]\" in content\n    assert 'black = \"*\"' in content\n    assert \"[feature.dev.target.linux-64.dependencies]\" in content\n    assert \"[feature.dev.target.win-64.dependencies]\" in content\n\n    assert \"[feature.docs.dependencies]\" in content\n    assert 'sphinx = \"*\"' in content\n\n    assert \"[environments]\" in content\n    assert \"default = []\" in content\n    assert \"dev = [\" in content\n    assert \"docs = [\" in content\n    assert \"all = [\" in content\n\n\ndef test_pixi_optional_dependencies_single_group(tmp_path: Path) -> None:\n    \"\"\"Test single optional group doesn't create 'all' environment.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        optional_dependencies:\n          test:\n            - pytest\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(\n        req_file,\n        project_name=\"test-project\",\n        output_file=output_file,\n        verbose=False,\n    )\n\n    content = output_file.read_text()\n\n    assert \"[feature.test.dependencies]\" in content\n    assert 'pytest = \"*\"' in content\n    assert \"all = [\" not in content\n\n\ndef test_pixi_single_file_optional_group_named_all_keeps_unique_env(\n    tmp_path: Path,\n) -> None:\n    \"\"\"A user-defined optional group named 'all' should not be overwritten.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        optional_dependencies:\n          all:\n            - pandas\n          dev:\n            - pytest\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", req_file)\n\n    assert \"all\" in data[\"feature\"]\n    assert \"dev\" in data[\"feature\"]\n\n    envs = data[\"environments\"]\n    assert envs[\"all\"] == [\"all\", \"dev\"]\n    user_all_envs = [name for name, feats in envs.items() if feats == [\"all\"]]\n    assert len(user_all_envs) == 1\n    assert user_all_envs[0] != \"all\"\n\n\ndef test_pixi_single_file_optional_local_dependency_stays_optional(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Optional local deps should appear in optional features, not root deps.\"\"\"\n    local_dep_dir = tmp_path / \"localdep\"\n    local_dep_dir.mkdir()\n    _write_file(\n        local_dep_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        \"\"\",\n    )\n\n    root_req = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        optional_dependencies:\n          dev:\n            - ./localdep\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", root_req)\n\n    assert data[\"dependencies\"][\"numpy\"] == \"*\"\n    assert \"pandas\" not in data.get(\"dependencies\", {})\n    assert data[\"feature\"][\"dev\"][\"dependencies\"][\"pandas\"] == \"*\"\n    assert data[\"environments\"][\"default\"] == []\n    assert data[\"environments\"][\"dev\"] == [\"dev\"]\n\n\ndef test_pixi_optional_dependencies_monorepo(tmp_path: Path) -> None:\n    \"\"\"Test optional dependencies in monorepo setup.\"\"\"\n    project1_dir = tmp_path / \"project1\"\n    project1_dir.mkdir()\n    req1 = _write_file(\n        project1_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        optional_dependencies:\n          test:\n            - pytest\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    project2_dir = tmp_path / \"project2\"\n    project2_dir.mkdir()\n    req2 = _write_file(\n        project2_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        optional_dependencies:\n          lint:\n            - black\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(\n        req1,\n        req2,\n        project_name=\"monorepo\",\n        output_file=output_file,\n        verbose=False,\n    )\n\n    content = output_file.read_text()\n\n    assert \"[feature.project1.target.linux-64.dependencies]\" in content\n    assert 'numpy = \"*\"' in content\n    assert \"[feature.project2.target.linux-64.dependencies]\" in content\n    assert 'pandas = \"*\"' in content\n\n    assert \"[feature.project1-test.target.linux-64.dependencies]\" in content\n    assert 'pytest = \"*\"' in content\n    assert \"[feature.project2-lint.target.linux-64.dependencies]\" in content\n    assert 'black = \"*\"' in content\n\n\ndef test_pixi_monorepo_optional_local_dependency_is_only_in_optional_env(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Optional local projects should be included only in the optional env.\"\"\"\n    app_req, other_req = _setup_app_lib_other(\n        tmp_path,\n        \"\"\"\\\n        - ../lib\n        - pytest\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", app_req, other_req)\n\n    features = data[\"feature\"]\n    assert \"app\" in features\n    assert \"app-dev\" in features\n    assert \"lib\" in features\n    assert \"other\" in features\n\n    envs = data[\"environments\"]\n    assert \"lib\" not in envs[\"default\"]\n    assert \"lib\" in envs[\"app-dev\"]\n\n\ndef test_pixi_monorepo_optional_group_with_only_local_deps_creates_env(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Local-only optional groups should still create optional environments.\"\"\"\n    app_req, other_req = _setup_app_lib_other(\n        tmp_path,\n        \"\"\"\\\n        - ../lib\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", app_req, other_req)\n\n    features = data[\"feature\"]\n    envs = data[\"environments\"]\n\n    assert \"app\" in features\n    assert \"lib\" in features\n    assert \"other\" in features\n    assert \"app-dev\" not in features\n    assert \"app-dev\" in envs\n    assert \"lib\" not in envs[\"default\"]\n    assert \"lib\" in envs[\"app-dev\"]\n\n\ndef test_pixi_monorepo_optional_feature_name_collision_does_not_overwrite_base_feature(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Optional feature names must not overwrite existing base feature keys.\"\"\"\n    project_dir = tmp_path / \"project\"\n    project_dir.mkdir()\n    _write_file(\n        project_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        optional_dependencies:\n          dev:\n            - pytest\n        \"\"\",\n    )\n\n    project_dev_dir = tmp_path / \"project-dev\"\n    project_dev_dir.mkdir()\n    _write_file(\n        project_dev_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        project_dir / \"requirements.yaml\",\n        project_dev_dir / \"requirements.yaml\",\n    )\n\n    features = data[\"feature\"]\n    assert features[\"project-dev\"][\"dependencies\"][\"pandas\"] == \"*\"\n    assert features[\"project-dev-opt\"][\"dependencies\"][\"pytest\"] == \"*\"\n    assert data[\"environments\"][\"project-dev-opt\"] == [\"project\", \"project-dev-opt\"]\n\n\ndef test_pixi_monorepo_default_env_excludes_optional_features(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Ensure monorepo default env only includes base features.\"\"\"\n    project1_dir = tmp_path / \"project1\"\n    project1_dir.mkdir()\n    req1 = _write_file(\n        project1_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        optional_dependencies:\n          dev:\n            - pytest\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    project2_dir = tmp_path / \"project2\"\n    project2_dir.mkdir()\n    req2 = _write_file(\n        project2_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        req1,\n        req2,\n        project_name=\"monorepo\",\n    )\n\n    envs = data[\"environments\"]\n    assert set(envs[\"default\"]) == {\"project1\", \"project2\"}\n    assert \"project1-dev\" not in envs[\"default\"]\n    assert set(envs[\"project1-dev\"]) == {\"project1\", \"project1-dev\"}\n\n\ndef test_pixi_empty_platform_override_uses_file_platforms(tmp_path: Path) -> None:\n    \"\"\"Passing platforms=[] should fall back to platforms from requirements files.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        platforms:\n          - linux-64\n          - osx-arm64\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        req_file,\n        platforms=[],\n    )\n\n    assert set(data[\"workspace\"][\"platforms\"]) == {\"linux-64\", \"osx-arm64\"}\n\n\ndef test_pixi_monorepo_keeps_optional_groups_when_base_feature_empty(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Optional sub-features should be preserved even when base feature is empty.\"\"\"\n    project1 = tmp_path / \"project1\"\n    project1.mkdir()\n    req1 = _write_file(\n        project1 / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies: []\n        optional_dependencies:\n          docs:\n            - sphinx\n        \"\"\",\n    )\n\n    project2 = tmp_path / \"project2\"\n    project2.mkdir()\n    req2 = _write_file(\n        project2 / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", req1, req2)\n\n    features = data[\"feature\"]\n    assert \"project1\" not in features\n    assert features[\"project1-docs\"][\"dependencies\"][\"sphinx\"] == \"*\"\n    assert \"project2\" in features\n\n    envs = data[\"environments\"]\n    assert envs[\"default\"] == [\"project2\"]\n    assert envs[\"project1-docs\"] == [\"project1-docs\"]\n\n\ndef test_pixi_monorepo_skips_empty_optional_feature_group(tmp_path: Path) -> None:\n    \"\"\"Empty optional groups should not create empty sub-features.\"\"\"\n    project1 = tmp_path / \"project1\"\n    project1.mkdir()\n    req1 = _write_file(\n        project1 / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        optional_dependencies:\n          docs:\n            - pytest\n        \"\"\",\n    )\n\n    project2 = tmp_path / \"project2\"\n    project2.mkdir()\n    req2 = _write_file(\n        project2 / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        req1,\n        req2,\n        skip_dependencies=[\"pytest\"],\n    )\n\n    assert \"project1-docs\" not in data[\"feature\"]\n\n\ndef test_derive_feature_names_handles_commonpath_valueerror(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Feature naming should fall back when commonpath raises ValueError.\"\"\"\n    first = tmp_path / \"a\" / \"api\"\n    second = tmp_path / \"b\" / \"api\"\n    first.mkdir(parents=True)\n    second.mkdir(parents=True)\n    req1 = first / \"requirements.yaml\"\n    req2 = second / \"requirements.yaml\"\n    req1.write_text(\"dependencies: [numpy]\\n\")\n    req2.write_text(\"dependencies: [pandas]\\n\")\n\n    def _raise_commonpath(_: list[str]) -> str:\n        msg = \"boom\"\n        raise ValueError(msg)\n\n    monkeypatch.setattr(\"unidep._pixi.os.path.commonpath\", _raise_commonpath)\n    names = _derive_feature_names([req1, req2])\n    assert len(names) == 2\n    assert len(set(names)) == 2\n\n\ndef test_derive_feature_names_handles_relative_to_valueerror(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Feature naming should still be unique if relative_to raises ValueError.\"\"\"\n    root1 = tmp_path / \"a+b\" / \"api\"\n    root2 = tmp_path / \"a b\" / \"api\"\n    root3 = tmp_path / \"a@b\" / \"api\"\n    root1.mkdir(parents=True)\n    root2.mkdir(parents=True)\n    root3.mkdir(parents=True)\n    req1 = root1 / \"requirements.yaml\"\n    req2 = root2 / \"requirements.yaml\"\n    req3 = root3 / \"requirements.yaml\"\n    req1.write_text(\"dependencies: [numpy]\\n\")\n    req2.write_text(\"dependencies: [pandas]\\n\")\n    req3.write_text(\"dependencies: [scipy]\\n\")\n\n    path_type = type(tmp_path)\n\n    def _raise_relative_to(_self: Path, *_args: object, **_kwargs: object) -> Path:\n        msg = \"boom\"\n        raise ValueError(msg)\n\n    monkeypatch.setattr(path_type, \"relative_to\", _raise_relative_to)\n    names = _derive_feature_names([req1, req2, req3])\n    assert len(names) == 3\n    assert len(set(names)) == 3\n    assert any(name.endswith(\"-2\") for name in names)\n\n\ndef test_editable_dependency_path_relative_forms(tmp_path: Path) -> None:\n    \"\"\"Editable path helper should preserve '.' and '../' relative forms.\"\"\"\n    project_dir = tmp_path / \"pkg\"\n    project_dir.mkdir()\n    same_dir_output = project_dir / \"pixi.toml\"\n    assert _editable_dependency_path(project_dir, same_dir_output) == \".\"\n\n    nested_output = tmp_path / \"nested\" / \"pixi.toml\"\n    nested_output.parent.mkdir()\n    assert _editable_dependency_path(project_dir, nested_output) == \"../pkg\"\n\n\ndef test_editable_dependency_path_cross_drive(\n    tmp_path: Path,\n    monkeypatch: Any,\n) -> None:\n    \"\"\"On Windows, cross-drive paths should fall back to absolute instead of crashing.\"\"\"\n    project_dir = tmp_path / \"pkg\"\n    project_dir.mkdir()\n    output = tmp_path / \"pixi.toml\"\n\n    original_relpath = os.path.relpath\n\n    def raising_relpath(_path: Any, _start: Any = None) -> str:\n        msg = \"path is on mount 'C:', start on mount 'D:'\"\n        raise ValueError(msg)\n\n    monkeypatch.setattr(os.path, \"relpath\", raising_relpath)\n    result = _editable_dependency_path(project_dir, output)\n    assert project_dir.resolve().as_posix() == result\n\n    monkeypatch.setattr(os.path, \"relpath\", original_relpath)\n    assert _editable_dependency_path(project_dir, output) == \"./pkg\"\n\n\ndef test_discover_local_dependency_graph_skips_non_local_and_missing(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Graph discovery should ignore skipped/pypi/missing local entries safely.\"\"\"\n    root = tmp_path / \"root\"\n    root.mkdir()\n    req = _write_file(\n        root / \"requirements.yaml\",\n        \"\"\"\\\n        dependencies:\n          - numpy\n        local_dependencies:\n          - local: ../missing\n            use: local\n          - local: ../skipme\n            use: skip\n          - local: ../pypi-alt\n            use: pypi\n            pypi: foo>=1\n        \"\"\",\n    )\n\n    result = _discover_local_dependency_graph([req])\n    assert result.roots == result.discovered\n    assert len(result.roots) == 1\n    assert result.graph[result.roots[0]] == []\n    assert result.optional_group_graph == {}\n    assert result.unmanaged_local_graph[result.roots[0]] == []\n    assert result.optional_group_unmanaged_graph == {}\n\n\n# --- Parametrized _parse_direct_requirements_for_node tests ---\n\n\n@pytest.mark.parametrize(\n    (\"extras\", \"req_content\", \"expected_in\"),\n    [\n        pytest.param(\n            [\"dev\"],\n            \"\"\"\\\n            dependencies:\n              - numpy\n            optional_dependencies:\n              dev:\n                - pytest\n            \"\"\",\n            [\"numpy\", \"pytest\"],\n            id=\"selected-extras\",\n        ),\n        pytest.param(\n            [\"*\"],\n            \"\"\"\\\n            dependencies:\n              - numpy\n            optional_dependencies:\n              dev:\n                - pytest\n              docs:\n                - sphinx\n            \"\"\",\n            [\"numpy\", \"pytest\", \"sphinx\"],\n            id=\"star-extra\",\n        ),\n    ],\n)\ndef test_parse_direct_requirements_for_node_extras(\n    tmp_path: Path,\n    extras: list[str],\n    req_content: str,\n    expected_in: list[str],\n) -> None:\n    \"\"\"Extras on a local node should merge into required dependencies.\"\"\"\n    req = _write_file(tmp_path / \"requirements.yaml\", req_content)\n    node = PathWithExtras(req, extras)\n    parsed = _parse_direct_requirements_for_node(\n        node,\n        verbose=False,\n        ignore_pins=None,\n        skip_dependencies=None,\n        overwrite_pins=None,\n    )\n    for name in expected_in:\n        assert name in parsed.requirements\n    assert parsed.optional_dependencies == {}\n\n\ndef test_collect_transitive_nodes_deduplicates_seen_nodes(tmp_path: Path) -> None:\n    \"\"\"Transitive collection should skip already-seen nodes in cyclic graphs.\"\"\"\n    req_a = PathWithExtras(tmp_path / \"a\" / \"requirements.yaml\", [])\n    req_b = PathWithExtras(tmp_path / \"b\" / \"requirements.yaml\", [])\n    graph = {req_a: [req_b, req_b], req_b: [req_a]}\n    collected = _collect_transitive_nodes(req_a, graph)\n    assert collected == [req_b, req_a]\n\n\ndef test_pixi_with_build_string(tmp_path: Path) -> None:\n    \"\"\"Test pixi.toml generation with build strings in version specs.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - conda: qsimcirq >=0.21.0 cuda*  # [linux64]\n          - gcc =11\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(req_file, output_file=output_file, verbose=False)\n\n    content = output_file.read_text()\n\n    assert \"[dependencies.qsimcirq]\" in content\n    assert 'version = \">=0.21.0\"' in content\n    assert 'build = \"cuda*\"' in content\n    assert 'gcc = \"=11\"' in content\n\n\ndef test_pixi_with_pip_extras(tmp_path: Path) -> None:\n    \"\"\"Test pixi.toml generation with pip extras.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pip: pipefunc[extras]\n          - pip: package[dev,test] >=1.0\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(req_file, output_file=output_file, verbose=False)\n\n    content = output_file.read_text()\n\n    assert \"[pypi-dependencies.pipefunc]\" in content\n    assert 'version = \"*\"' in content\n    assert '\"extras\"' in content\n\n    assert \"[pypi-dependencies.package]\" in content\n    assert 'version = \">=1.0\"' in content\n    assert '\"dev\"' in content\n    assert '\"test\"' in content\n\n\ndef test_pixi_with_merged_constraints(tmp_path: Path) -> None:\n    \"\"\"Test pixi.toml generation merges version constraints.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - scipy >=1.7,<2\n          - scipy <1.16\n          - numpy >=1.20\n          - numpy <2.0\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    output_file = tmp_path / \"pixi.toml\"\n    generate_pixi_toml(req_file, output_file=output_file, verbose=False)\n\n    content = output_file.read_text()\n\n    assert 'scipy = \">=1.7,<1.16\"' in content\n    assert 'numpy = \">=1.20,<2.0\"' in content\n\n\ndef test_pixi_optional_local_dep_does_not_leak_base_local_deps(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Base local deps must not appear in optional features.\"\"\"\n    lib1 = tmp_path / \"lib1\"\n    lib1.mkdir()\n    _write_file(\n        lib1 / \"requirements.yaml\",\n        \"\"\"\\\n        dependencies:\n          - pandas\n          - scipy\n        \"\"\",\n    )\n\n    lib2 = tmp_path / \"lib2\"\n    lib2.mkdir()\n    _write_file(\n        lib2 / \"requirements.yaml\",\n        \"\"\"\\\n        dependencies:\n          - requests\n        \"\"\",\n    )\n\n    root_req = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        dependencies:\n          - numpy\n        local_dependencies:\n          - ./lib1\n        optional_dependencies:\n          dev:\n            - ./lib2\n            - pytest\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", root_req)\n\n    root_deps = set(data.get(\"dependencies\", {}).keys())\n    dev_deps = set(data[\"feature\"][\"dev\"].get(\"dependencies\", {}).keys())\n\n    assert \"pandas\" in root_deps\n    assert \"scipy\" in root_deps\n    assert \"pandas\" not in dev_deps, \"base local dep leaked into optional feature\"\n    assert \"scipy\" not in dev_deps, \"base local dep leaked into optional feature\"\n    assert \"requests\" in dev_deps\n    assert \"pytest\" in dev_deps\n\n\n# --- Parametrized demotion weak-target tests ---\n\n\n@pytest.mark.parametrize(\n    (\"deps_yaml\", \"osx_dep_section\", \"osx_click_val\"),\n    [\n        pytest.param(\n            \"\"\"\\\n            - conda: click >=8\n            - pip: click ==0.1  # [linux64]\n            - pip: click        # [osx64]\n            \"\"\",\n            \"dependencies\",\n            \">=8\",\n            id=\"pinned-narrower-pip-beats-universal-conda\",\n        ),\n        pytest.param(\n            \"\"\"\\\n            - conda: click\n            - pip: click ==0.1  # [linux64]\n            - pip: click        # [osx64]\n            \"\"\",\n            \"dependencies\",\n            \"*\",\n            id=\"unpinned-universal-conda-beats-unpinned-target-pip\",\n        ),\n    ],\n)\ndef test_pixi_demoted_universal_weak_target(\n    tmp_path: Path,\n    deps_yaml: str,\n    osx_dep_section: str,\n    osx_click_val: str,\n) -> None:\n    \"\"\"Demoted universals should replace weak target overrides correctly.\"\"\"\n    deps_block = textwrap.indent(textwrap.dedent(deps_yaml), \"  \")\n    yaml_content = (\n        \"channels:\\n\"\n        \"  - conda-forge\\n\"\n        \"dependencies:\\n\"\n        f\"{deps_block}\"\n        \"platforms:\\n\"\n        \"  - linux-64\\n\"\n        \"  - osx-64\\n\"\n    )\n    req_file = tmp_path / \"requirements.yaml\"\n    req_file.write_text(yaml_content)\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", req_file)\n\n    linux = data[\"target\"][\"linux-64\"]\n    assert linux[\"pypi-dependencies\"][\"click\"] == \"==0.1\"\n\n    osx = data[\"target\"][\"osx-64\"]\n    assert osx[osx_dep_section][\"click\"] == osx_click_val\n    if osx_dep_section == \"pypi-dependencies\":\n        assert \"click\" not in osx.get(\"dependencies\", {})\n    else:\n        assert \"click\" not in osx.get(\"pypi-dependencies\", {})\n\n\ndef test_pixi_demoted_universal_uses_latest_merged_constraint(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Repeated universal specs must not leave a stale weaker constraint in demoted.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - conda: click >=8\n          - pip: click ==0.1  # [linux64]\n          - conda: click >=9\n        platforms:\n          - linux-64\n          - osx-64\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", req_file)\n\n    # linux-64 should keep the target-specific pip override\n    assert data[\"target\"][\"linux-64\"][\"pypi-dependencies\"][\"click\"] == \"==0.1\"\n\n    # osx-64 must get the final merged constraint (>=9), NOT the stale first (>=8)\n    assert data[\"target\"][\"osx-64\"][\"dependencies\"][\"click\"] == \">=9\"\n    assert \"click\" not in data[\"target\"][\"osx-64\"].get(\"pypi-dependencies\", {})\n\n    # Universal should be empty (demoted to per-platform targets)\n    assert \"click\" not in data.get(\"dependencies\", {})\n    assert \"click\" not in data.get(\"pypi-dependencies\", {})\n\n\ndef test_pixi_demoted_universal_merges_constraints_across_demotions(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Demoted universal constraints should keep cumulative merged bounds.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - conda: click >=8\n          - pip: click ==0.1  # [linux64]\n          - conda: click <=10\n        platforms:\n          - linux-64\n          - osx-64\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", req_file)\n\n    assert data[\"target\"][\"linux-64\"][\"pypi-dependencies\"][\"click\"] == \"==0.1\"\n    assert data[\"target\"][\"osx-64\"][\"dependencies\"][\"click\"] == \">=8,<=10\"\n\n\ndef test_pixi_raises_when_losing_pip_alternative_is_internally_contradictory(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Contradictory pip alternatives should fail even if conda would otherwise win.\"\"\"\n    req_file = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - conda: click >=8\n          - pip: click ==0.1 # [linux64]\n          - pip: click >=8\n          - conda: click >=9 # [linux64]\n        platforms:\n          - linux-64\n          - osx-64\n        \"\"\",\n    )\n\n    with pytest.raises(VersionConflictError, match=\"click\"):\n        _generate_and_load(tmp_path / \"pixi.toml\", req_file)\n\n\ndef test_parse_version_build_whitespace_only() -> None:\n    assert _parse_version_build(\"  \") == \"*\"\n\n\ndef test_make_pip_version_spec_dict_with_extras() -> None:\n    result = _make_pip_version_spec({\"version\": \">=1.0\", \"build\": \"py3*\"}, [\"extra1\"])\n    assert result == {\"version\": \">=1.0\", \"build\": \"py3*\", \"extras\": [\"extra1\"]}\n\n\ndef test_with_unique_order_paths_deduplicates(tmp_path: Path) -> None:\n    d = tmp_path / \"a\"\n    d.mkdir()\n    result = _with_unique_order_paths([d, d, d])\n    assert result == [d]\n\n\ndef test_unique_optional_feature_name_double_collision() -> None:\n    taken: set[str] = {\"feat-dev\", \"feat-dev-opt\"}\n    name = _unique_optional_feature_name(\n        parent_feature=\"feat\",\n        group_name=\"dev\",\n        taken_names=taken,\n    )\n    assert name == \"feat-dev-opt-2\"\n    assert name in taken\n\n\ndef test_unique_env_name_triple_collision() -> None:\n    taken: set[str] = {\"foo-bar\", \"foo-bar-2\"}\n    assert _unique_env_name(\"foo_bar\", taken) == \"foo-bar-3\"\n\n\ndef test_add_single_file_optional_environments_noop_without_features() -> None:\n    pixi_data: dict[str, Any] = {\"environments\": {}}\n    _add_single_file_optional_environments(pixi_data, [])\n    assert pixi_data == {\"environments\": {}}\n\n\ndef test_feature_platforms_for_entries_prefers_override() -> None:\n    origin = DependencyOrigin(Path(\"requirements.yaml\"), 0)\n    entries = [\n        DependencyEntry(\n            identifier=\"numpy\",\n            selector=None,\n            conda=Spec(name=\"numpy\", which=\"conda\"),\n            pip=None,\n            origin=origin,\n        ),\n    ]\n\n    assert _feature_platforms_for_entries(\n        entries=entries,\n        declared_platforms=[\"linux-64\"],\n        global_declared_platforms={\"osx-arm64\"},\n        platforms_override=[\"win-64\"],\n    ) == [\"win-64\"]\n\n\ndef test_extract_dependencies_handles_universal_pip_and_mixed_buckets() -> None:\n    origin = DependencyOrigin(Path(\"requirements.yaml\"), 0)\n    entries = [\n        DependencyEntry(\n            identifier=\"numpy\",\n            selector=None,\n            conda=Spec(name=\"numpy\", which=\"conda\"),\n            pip=None,\n            origin=origin,\n        ),\n        DependencyEntry(\n            identifier=\"click\",\n            selector=\"linux64\",\n            conda=None,\n            pip=Spec(name=\"click\", which=\"pip\", selector=\"linux64\"),\n            origin=origin,\n        ),\n    ]\n\n    deps = _extract_dependencies(\n        entries,\n        platforms=[\"linux-64\", \"osx-64\"],\n        allow_hoist_without_universal_origin=True,\n    )\n    assert deps[None][0][\"numpy\"] == \"*\"\n    assert deps[\"linux-64\"][1][\"click\"] == \"*\"\n    assert \"osx-64\" not in deps or \"click\" not in deps[\"osx-64\"][1]\n\n\ndef test_filter_targets_by_platforms_removes_empty_sections() -> None:\n    pixi_data: dict[str, Any] = {\n        \"target\": {\n            \"osx-64\": {\"dependencies\": {\"numpy\": \"*\"}},\n        },\n        \"feature\": {\n            \"dev\": {\n                \"target\": {\n                    \"linux-64\": {\"dependencies\": {\"pytest\": \"*\"}},\n                },\n            },\n        },\n    }\n\n    _filter_targets_by_platforms(pixi_data, {\"osx-arm64\"})\n\n    assert \"target\" not in pixi_data\n    assert \"target\" not in pixi_data[\"feature\"][\"dev\"]\n\n\ndef test_pixi_single_file_optional_local_dep_transitive_dedup(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Cover single-file optional local dep dedup and pip-installable path.\"\"\"\n    shared_dir = tmp_path / \"shared\"\n    shared_dir.mkdir()\n    _write_file(\n        shared_dir / \"requirements.yaml\",\n        \"\"\"\\\n        dependencies:\n          - scipy\n    \"\"\",\n    )\n    (shared_dir / \"setup.py\").write_text(\n        \"from setuptools import setup; setup(name='shared')\",\n    )\n\n    opt_a_dir = tmp_path / \"opt_a\"\n    opt_a_dir.mkdir()\n    _write_file(\n        opt_a_dir / \"requirements.yaml\",\n        \"\"\"\\\n        dependencies:\n          - pandas\n        local_dependencies:\n          - ../shared\n    \"\"\",\n    )\n    (opt_a_dir / \"setup.py\").write_text(\n        \"from setuptools import setup; setup(name='opt-a')\",\n    )\n\n    opt_b_dir = tmp_path / \"opt_b\"\n    opt_b_dir.mkdir()\n    _write_file(\n        opt_b_dir / \"requirements.yaml\",\n        \"\"\"\\\n        dependencies:\n          - polars\n        local_dependencies:\n          - ../shared\n    \"\"\",\n    )\n    (opt_b_dir / \"setup.py\").write_text(\n        \"from setuptools import setup; setup(name='opt-b')\",\n    )\n\n    (tmp_path / \"setup.py\").write_text(\n        \"from setuptools import setup; setup(name='root')\",\n    )\n    req = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        optional_dependencies:\n          extras:\n            - ./opt_a\n            - ./opt_b\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", req)\n\n    assert \"extras\" in data[\"feature\"]\n    extras_deps = data[\"feature\"][\"extras\"].get(\"dependencies\", {})\n    assert \"pandas\" in extras_deps or \"polars\" in extras_deps\n\n\ndef test_pixi_single_file_optional_group_demoted_universal(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Cover optional group's own deps trigger demotion.\"\"\"\n    req = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        optional_dependencies:\n          special:\n            - conda: click\n            - pip: click >=2.0  # [linux64]\n        platforms:\n          - linux-64\n          - osx-arm64\n        \"\"\",\n    )\n    data = _generate_and_load(tmp_path / \"pixi.toml\", req)\n\n    special = data[\"feature\"][\"special\"]\n    assert special[\"target\"][\"linux-64\"][\"pypi-dependencies\"][\"click\"] == \">=2.0\"\n    assert special[\"target\"][\"osx-arm64\"][\"dependencies\"][\"click\"] == \"*\"\n    assert \"click\" not in special.get(\"dependencies\", {})\n    assert \"click\" not in special.get(\"pypi-dependencies\", {})\n\n\n# --- Parametrized monorepo demotion tests ---\n\n\n@pytest.mark.parametrize(\n    (\n        \"proj_deps\",\n        \"proj_feature_key\",\n        \"universal_pkg\",\n        \"linux_pip_val\",\n        \"osx_conda_val\",\n    ),\n    [\n        pytest.param(\n            \"\"\"\\\n            channels:\n              - conda-forge\n            dependencies:\n              - conda: requests\n              - pip: requests >=2.0  # [linux64]\n            platforms:\n              - linux-64\n              - osx-arm64\n            \"\"\",\n            \"proj\",\n            \"requests\",\n            \">=2.0\",\n            \"*\",\n            id=\"feature-demoted-universal\",\n        ),\n        pytest.param(\n            \"\"\"\\\n            channels:\n              - conda-forge\n            dependencies:\n              - numpy\n            optional_dependencies:\n              special:\n                - conda: click\n                - pip: click >=2.0  # [linux64]\n            platforms:\n              - linux-64\n              - osx-arm64\n            \"\"\",\n            \"proj-special\",\n            \"click\",\n            \">=2.0\",\n            \"*\",\n            id=\"optional-group-demoted\",\n        ),\n    ],\n)\ndef test_pixi_monorepo_demotion(\n    tmp_path: Path,\n    proj_deps: str,\n    proj_feature_key: str,\n    universal_pkg: str,\n    linux_pip_val: str,\n    osx_conda_val: str,\n) -> None:\n    \"\"\"Monorepo feature/optional-group demotion + restore.\"\"\"\n    proj = tmp_path / \"proj\"\n    proj.mkdir()\n    _write_file(proj / \"requirements.yaml\", proj_deps)\n\n    proj2 = tmp_path / \"proj2\"\n    proj2.mkdir()\n    _write_file(\n        proj2 / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        proj / \"requirements.yaml\",\n        proj2 / \"requirements.yaml\",\n    )\n\n    feature = data[\"feature\"][proj_feature_key]\n    assert (\n        feature[\"target\"][\"linux-64\"][\"pypi-dependencies\"][universal_pkg]\n        == linux_pip_val\n    )\n    assert (\n        feature[\"target\"][\"osx-arm64\"][\"dependencies\"][universal_pkg] == osx_conda_val\n    )\n    assert universal_pkg not in feature.get(\"dependencies\", {})\n    assert universal_pkg not in feature.get(\"pypi-dependencies\", {})\n    assert (\n        data[\"feature\"][\"proj2\"][\"target\"][\"linux-64\"][\"dependencies\"][\"pandas\"] == \"*\"\n    )\n\n\ndef test_pixi_monorepo_feature_subset_does_not_leak_universal_deps(\n    tmp_path: Path,\n) -> None:\n    proj = tmp_path / \"proj\"\n    proj.mkdir()\n    _write_file(\n        proj / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        platforms:\n          - osx-arm64\n        \"\"\",\n    )\n\n    proj2 = tmp_path / \"proj2\"\n    proj2.mkdir()\n    _write_file(\n        proj2 / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - click\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        proj / \"requirements.yaml\",\n        proj2 / \"requirements.yaml\",\n    )\n\n    feature = data[\"feature\"][\"proj\"]\n    assert \"pandas\" not in feature.get(\"dependencies\", {})\n    assert feature[\"target\"][\"osx-arm64\"][\"dependencies\"][\"pandas\"] == \"*\"\n    assert \"linux-64\" not in feature.get(\"target\", {})\n\n\ndef test_pixi_single_file_env_name_collision(tmp_path: Path) -> None:\n    \"\"\"Optional groups whose names collide after underscore-to-hyphen normalization.\"\"\"\n    req = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        optional_dependencies:\n          foo_bar:\n            - pandas\n          foo-bar:\n            - polars\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n    data = _generate_and_load(tmp_path / \"pixi.toml\", req)\n\n    envs = data[\"environments\"]\n    env_feature_lists = [v for k, v in envs.items() if k not in (\"default\", \"all\")]\n    flat_features = [feat for lst in env_feature_lists for feat in lst]\n    assert \"foo_bar\" in flat_features\n    assert \"foo-bar\" in flat_features\n    assert len(env_feature_lists) == 2\n\n\ndef test_pixi_discover_graph_skips_non_list_optional_group(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Cover optional group dep that is not a list.\"\"\"\n    req = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        dependencies:\n          - numpy\n        optional_dependencies:\n          bad_group: \"not a list\"\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n    result = _discover_local_dependency_graph([req])\n    assert len(result.roots) == 1\n    assert not result.optional_group_graph\n\n\ndef test_pixi_discover_graph_skips_non_local_optional_dep(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Cover optional dep with use != local via override side-effect.\"\"\"\n    proj = tmp_path / \"proj\"\n    proj.mkdir()\n    other = tmp_path / \"other\"\n    other.mkdir()\n    (other / \"setup.py\").write_text(\n        \"from setuptools import setup; setup(name='other')\",\n    )\n    _write_file(\n        other / \"requirements.yaml\",\n        \"\"\"\\\n        dependencies:\n          - scipy\n        \"\"\",\n    )\n    _write_file(\n        proj / \"requirements.yaml\",\n        \"\"\"\\\n        dependencies:\n          - numpy\n        local_dependencies:\n          - local: ../other\n            use: pypi\n            pypi: other-pkg\n        optional_dependencies:\n          extras:\n            - ../other\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n    result = _discover_local_dependency_graph(\n        [proj / \"requirements.yaml\"],\n    )\n    assert len(result.roots) == 1\n    assert not result.optional_group_graph.get(result.roots[0], {}).get(\"extras\", [])\n\n\ndef test_pixi_discover_graph_skips_non_installable_optional_unmanaged(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Cover optional unmanaged dep that is not pip-installable.\"\"\"\n    proj = tmp_path / \"proj\"\n    proj.mkdir()\n    not_installable = tmp_path / \"nosetup\"\n    not_installable.mkdir()\n    _write_file(\n        proj / \"requirements.yaml\",\n        \"\"\"\\\n        dependencies:\n          - numpy\n        optional_dependencies:\n          extras:\n            - ../nosetup\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n    result = _discover_local_dependency_graph(\n        [proj / \"requirements.yaml\"],\n    )\n    assert len(result.roots) == 1\n    assert not result.optional_group_graph.get(result.roots[0], {}).get(\"extras\", [])\n    assert not result.optional_group_unmanaged_graph.get(result.roots[0], {}).get(\n        \"extras\",\n        [],\n    )\n\n\ndef test_restore_demoted_skips_when_still_in_universal(tmp_path: Path) -> None:\n    \"\"\"Cover restore skips when pkg is in universal deps or target.\"\"\"\n    req = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - conda: numpy >=1.0\n          - pip: numpy >=2.0  # [linux64]\n          - conda: scipy\n        platforms:\n          - linux-64\n          - osx-arm64\n        \"\"\",\n    )\n    data = _generate_and_load(tmp_path / \"pixi.toml\", req)\n    assert \"scipy\" in data[\"dependencies\"]\n\n\ndef test_pixi_monorepo_optional_local_feature_not_in_pixi_data(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Cover optional local dep feature not in pixi_data.\"\"\"\n    empty_dir = tmp_path / \"empty\"\n    empty_dir.mkdir()\n    _write_file(\n        empty_dir / \"requirements.yaml\",\n        \"\"\"\\\n        dependencies: []\n    \"\"\",\n    )\n\n    proj = tmp_path / \"proj\"\n    proj.mkdir()\n    _write_file(\n        proj / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        optional_dependencies:\n          extras:\n            - ../empty\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    proj2 = tmp_path / \"proj2\"\n    proj2.mkdir()\n    _write_file(\n        proj2 / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        proj / \"requirements.yaml\",\n        proj2 / \"requirements.yaml\",\n    )\n    for env_features in data.get(\"environments\", {}).values():\n        assert \"empty\" not in env_features\n\n\ndef test_pixi_single_file_installable_optional_local_dep_not_in_root(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Pip-installable optional local deps must NOT leak into root pypi-dependencies.\"\"\"\n    localdep_dir = tmp_path / \"localdep\"\n    localdep_dir.mkdir()\n    _write_file(\n        localdep_dir / \"requirements.yaml\",\n        \"\"\"\\\n        dependencies:\n          - pandas\n        \"\"\",\n    )\n    (localdep_dir / \"setup.py\").write_text(\n        \"from setuptools import setup; setup(name='localdep')\",\n    )\n\n    root_req = _write_file(\n        tmp_path / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        optional_dependencies:\n          dev:\n            - ./localdep\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    data = _generate_and_load(tmp_path / \"pixi.toml\", root_req)\n\n    # localdep must NOT appear in root pypi-dependencies\n    root_pypi = data.get(\"pypi-dependencies\", {})\n    assert \"localdep\" not in root_pypi, (\n        \"pip-installable optional local dep leaked into root pypi-dependencies\"\n    )\n\n    # localdep MUST appear in the dev feature\n    dev_pypi = data[\"feature\"][\"dev\"].get(\"pypi-dependencies\", {})\n    assert \"localdep\" in dev_pypi, \"optional local dep missing from dev feature\"\n    assert dev_pypi[\"localdep\"][\"editable\"] is True\n\n\ndef test_pixi_monorepo_optional_aggregator_transitive_deps_in_env(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Empty aggregator in optional group must still pull transitive features into env.\"\"\"\n    lib_dir = tmp_path / \"lib\"\n    lib_dir.mkdir()\n    _write_file(\n        lib_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - scipy\n        \"\"\",\n    )\n\n    agg_dir = tmp_path / \"agg\"\n    agg_dir.mkdir()\n    _write_file(\n        agg_dir / \"requirements.yaml\",\n        \"\"\"\\\n        dependencies: []\n        local_dependencies:\n          - ../lib\n        \"\"\",\n    )\n\n    app_dir = tmp_path / \"app\"\n    app_dir.mkdir()\n    _write_file(\n        app_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - numpy\n        optional_dependencies:\n          extras:\n            - ../agg\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    other_dir = tmp_path / \"other\"\n    other_dir.mkdir()\n    _write_file(\n        other_dir / \"requirements.yaml\",\n        \"\"\"\\\n        channels:\n          - conda-forge\n        dependencies:\n          - pandas\n        platforms:\n          - linux-64\n        \"\"\",\n    )\n\n    data = _generate_and_load(\n        tmp_path / \"pixi.toml\",\n        app_dir / \"requirements.yaml\",\n        other_dir / \"requirements.yaml\",\n    )\n\n    app_extras_env = data[\"environments\"].get(\"app-extras\", [])\n    assert \"lib\" in app_extras_env, (\n        f\"transitive dep 'lib' missing from app-extras env: {app_extras_env}\"\n    )\n"
  },
  {
    "path": "tests/test_project_dependency_handling.py",
    "content": "\"\"\"Tests for the `project_dependency_handling` feature.\"\"\"\n\nfrom __future__ import annotations\n\nimport textwrap\nfrom typing import TYPE_CHECKING, Literal\n\nimport pytest\n\nfrom unidep._dependencies_parsing import (\n    _add_project_dependencies,\n    parse_requirements,\n)\nfrom unidep.platform_definitions import Spec\n\nif TYPE_CHECKING:\n    from pathlib import Path\n\n\n@pytest.mark.parametrize(\n    (\"project_dependencies\", \"handling_mode\", \"expected\"),\n    [\n        # Test same-name\n        (\n            [\"pandas\", \"requests\"],\n            \"same-name\",\n            [\"pandas\", \"requests\"],\n        ),\n        # Test pip-only\n        (\n            [\"pandas\", \"requests\"],\n            \"pip-only\",\n            [{\"pip\": \"pandas\"}, {\"pip\": \"requests\"}],\n        ),\n        # Test ignore\n        ([\"pandas\", \"requests\"], \"ignore\", []),\n        # Test invalid handling mode\n        ([\"pandas\", \"requests\"], \"invalid\", []),\n    ],\n)\ndef test_project_dependency_handling(\n    project_dependencies: list[str],\n    handling_mode: Literal[\"same-name\", \"pip-only\", \"ignore\", \"invalid\"],\n    expected: list[dict[str, str] | str],\n) -> None:\n    valid_unidep_dependencies: list[dict[str, str] | str] = [\n        {\"conda\": \"pandas\", \"pip\": \"pandas\"},\n        \"requests\",\n        {\"conda\": \"zstd\", \"pip\": \"zstandard\"},\n    ]\n    unidep_dependencies = valid_unidep_dependencies.copy()\n    if handling_mode == \"invalid\":\n        with pytest.raises(ValueError, match=\"Invalid `project_dependency_handling`\"):\n            _add_project_dependencies(\n                project_dependencies,\n                unidep_dependencies,\n                handling_mode,  # type: ignore[arg-type]\n            )\n    else:\n        _add_project_dependencies(\n            project_dependencies,\n            unidep_dependencies,\n            handling_mode,  # type: ignore[arg-type]\n        )\n        assert unidep_dependencies == valid_unidep_dependencies + expected\n\n\n@pytest.mark.parametrize(\n    \"project_dependency_handling\",\n    [\"same-name\", \"pip-only\", \"ignore\"],\n)\ndef test_project_dependency_handling_in_pyproject_toml(\n    tmp_path: Path,\n    project_dependency_handling: Literal[\"same-name\", \"pip-only\", \"ignore\"],\n) -> None:\n    p = tmp_path / \"pyproject.toml\"\n    p.write_text(\n        textwrap.dedent(\n            f\"\"\"\\\n            [build-system]\n            requires = [\"hatchling\", \"unidep\"]\n            build-backend = \"hatchling.build\"\n\n            [project]\n            name = \"my-project\"\n            version = \"0.1.0\"\n            dependencies = [\n                \"requests\",\n                \"pandas\",\n            ]\n\n            [tool.unidep]\n            project_dependency_handling = \"{project_dependency_handling}\"\n            dependencies = [\n                {{ conda = \"python-graphviz\", pip = \"graphviz\" }},\n                {{ conda = \"graphviz\" }},\n            ]\n            \"\"\",\n        ),\n    )\n\n    requirements = parse_requirements(p)\n\n    expected = {\n        \"python-graphviz\": [\n            Spec(name=\"python-graphviz\", which=\"conda\", identifier=\"17e5d607\"),\n        ],\n        \"graphviz\": [\n            Spec(name=\"graphviz\", which=\"pip\", identifier=\"17e5d607\"),\n            Spec(name=\"graphviz\", which=\"conda\", identifier=\"5eb93b8c\"),\n        ],\n    }\n    if project_dependency_handling == \"pip-only\":\n        expected.update(\n            {\n                \"requests\": [Spec(name=\"requests\", which=\"pip\", identifier=\"08fd8713\")],\n                \"pandas\": [Spec(name=\"pandas\", which=\"pip\", identifier=\"9e467fa1\")],\n            },\n        )\n    elif project_dependency_handling == \"same-name\":\n        expected.update(\n            {\n                \"requests\": [\n                    Spec(name=\"requests\", which=\"conda\", identifier=\"08fd8713\"),\n                    Spec(name=\"requests\", which=\"pip\", identifier=\"08fd8713\"),\n                ],\n                \"pandas\": [\n                    Spec(name=\"pandas\", which=\"conda\", identifier=\"9e467fa1\"),\n                    Spec(name=\"pandas\", which=\"pip\", identifier=\"9e467fa1\"),\n                ],\n            },\n        )\n    else:\n        assert project_dependency_handling == \"ignore\"\n    assert requirements.requirements == expected\n"
  },
  {
    "path": "tests/test_pypi_alternatives/main_app/main_app/__init__.py",
    "content": "\"\"\"Main application module.\"\"\"\n\n\ndef main() -> str:\n    \"\"\"Run the main application logic.\"\"\"\n    from shared_lib import greet\n\n    return f\"Main app says: {greet()}\"\n"
  },
  {
    "path": "tests/test_pypi_alternatives/main_app/pyproject.toml",
    "content": "[build-system]\nrequires = [\"hatchling\", \"unidep @ file:///Users/bas.nijholt/Work/unidep\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\nname = \"test-main-app\"\nversion = \"0.1.0\"\ndescription = \"Main app testing PyPI alternatives\"\ndynamic = [\"dependencies\"]\n\n[tool.hatch.metadata]\nallow-direct-references = true\n\n[tool.hatch.metadata.hooks.unidep]\n\n[tool.hatch.build.targets.wheel]\npackages = [\"main_app\"]\n\n[tool.unidep]\ndependencies = [\"numpy\"]\nlocal_dependencies = [\n    {local = \"../shared_lib\", pypi = \"pipefunc\"}\n]\n"
  },
  {
    "path": "tests/test_pypi_alternatives/shared_lib/pyproject.toml",
    "content": "[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\nname = \"test-shared-lib\"\nversion = \"1.0.0\"\ndescription = \"Shared library for testing PyPI alternatives\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"shared_lib\"]\n"
  },
  {
    "path": "tests/test_pypi_alternatives/shared_lib/shared_lib/__init__.py",
    "content": "\"\"\"Shared library module.\"\"\"\n\n\ndef greet() -> str:\n    \"\"\"Return a greeting message.\"\"\"\n    return \"Hello from LOCAL shared library!\"\n"
  },
  {
    "path": "tests/test_pypi_alternatives/test_all_scenarios.sh",
    "content": "#!/bin/bash\n# Test PyPI alternatives feature in different scenarios\n\nset -e  # Exit on error\n\necho \"=== Testing PyPI Alternatives Feature ===\"\necho\n\nexport UV_NO_CACHE=1\n\n# Colors for output\nGREEN='\\033[0;32m'\nRED='\\033[0;31m'\nYELLOW='\\033[1;33m'\nNC='\\033[0m' # No Color\n\n# Function to extract and show dependencies\nshow_dependencies() {\n    local wheel_file=\"$1\"\n    local scenario=\"$2\"\n\n    echo -e \"${YELLOW}${scenario}${NC}\"\n    unzip -p \"$wheel_file\" '*/METADATA' | grep \"Requires-Dist:\" || echo \"No dependencies found\"\n    echo\n}\n\n# Clean up function\ncleanup() {\n    rm -rf main_app/dist\n    rm -rf test_main_app-0.1.0.dist-info\n}\n\n# Start fresh\necho \"Cleaning up previous builds...\"\ncleanup\n\n# Scenario 1: Normal build (local path exists)\necho -e \"${GREEN}=== Scenario 1: Normal build (local path exists) ===${NC}\"\necho \"Expected: Should use file:// URL\"\necho\ncd main_app\nuv build > /dev/null 2>&1\nshow_dependencies \"dist/test_main_app-0.1.0-py2.py3-none-any.whl\" \"Dependencies in wheel:\"\ncd ..\ncleanup\n\n# Scenario 2: Build with local path missing (simulating CI)\necho -e \"${GREEN}=== Scenario 2: Build with local path missing (CI simulation) ===${NC}\"\necho \"Expected: Should use PyPI alternative (pipefunc)\"\necho\nmv shared_lib shared_lib.tmp\ncd main_app\nuv build > /dev/null 2>&1\nshow_dependencies \"dist/test_main_app-0.1.0-py2.py3-none-any.whl\" \"Dependencies in wheel:\"\ncd ..\nmv shared_lib.tmp shared_lib\ncleanup\n\n# Scenario 3: Build with UNIDEP_SKIP_LOCAL_DEPS=1 (local path exists)\necho -e \"${GREEN}=== Scenario 3: Build with UNIDEP_SKIP_LOCAL_DEPS=1 (local path exists) ===${NC}\"\necho \"Expected: Should use PyPI alternative (pipefunc) even though local exists\"\necho\ncd main_app\nUNIDEP_SKIP_LOCAL_DEPS=1 uv build > /dev/null 2>&1\nshow_dependencies \"dist/test_main_app-0.1.0-py2.py3-none-any.whl\" \"Dependencies in wheel:\"\ncd ..\ncleanup\n\n# Scenario 4: Build with UNIDEP_SKIP_LOCAL_DEPS=1 and local path missing\necho -e \"${GREEN}=== Scenario 4: Build with UNIDEP_SKIP_LOCAL_DEPS=1 (local path missing) ===${NC}\"\necho \"Expected: Should use PyPI alternative (pipefunc)\"\necho\nmv shared_lib shared_lib.tmp\ncd main_app\nUNIDEP_SKIP_LOCAL_DEPS=1 uv build > /dev/null 2>&1\nshow_dependencies \"dist/test_main_app-0.1.0-py2.py3-none-any.whl\" \"Dependencies in wheel:\"\ncd ..\nmv shared_lib.tmp shared_lib\ncleanup\n"
  },
  {
    "path": "tests/test_pypi_alternatives.py",
    "content": "\"\"\"Test PyPI alternatives for local dependencies.\"\"\"\n\nfrom __future__ import annotations\n\nimport sys\nimport textwrap\nfrom typing import TYPE_CHECKING\n\nimport pytest\nfrom ruamel.yaml import YAML, YAMLError\n\nfrom unidep import parse_local_dependencies, parse_requirements\nfrom unidep._dependencies_parsing import (\n    LocalDependency,\n    _parse_local_dependency_item,\n    get_local_dependencies,\n    yaml_to_toml,\n)\nfrom unidep._setuptools_integration import get_python_dependencies\n\nfrom .helpers import maybe_as_toml\n\nif sys.version_info >= (3, 11):\n    import tomllib\nelse:  # pragma: no cover\n    import tomli as tomllib\n\nif TYPE_CHECKING:\n    from pathlib import Path\n\n    if sys.version_info >= (3, 8):\n        from typing import Literal\n    else:  # pragma: no cover\n        from typing_extensions import Literal\n\n\ndef test_parse_local_dependency_item_string() -> None:\n    \"\"\"Test parsing string format local dependency.\"\"\"\n    item = \"../foo\"\n    result = _parse_local_dependency_item(item)\n    assert result == LocalDependency(local=\"../foo\", pypi=None)\n\n\ndef test_parse_local_dependency_item_dict() -> None:\n    \"\"\"Test parsing dict format local dependency.\"\"\"\n    item = {\"local\": \"../foo\", \"pypi\": \"company-foo\"}\n    result = _parse_local_dependency_item(item)\n    assert result == LocalDependency(local=\"../foo\", pypi=\"company-foo\")\n\n\ndef test_parse_local_dependency_item_dict_with_use() -> None:\n    \"\"\"Test parsing dict format with explicit `use`.\"\"\"\n    item = {\"local\": \"../foo\", \"pypi\": \"company-foo\", \"use\": \"pypi\"}\n    result = _parse_local_dependency_item(item)\n    assert result == LocalDependency(\n        local=\"../foo\",\n        pypi=\"company-foo\",\n        use=\"pypi\",\n    )\n\n\ndef test_parse_local_dependency_item_dict_no_pypi() -> None:\n    \"\"\"Test parsing dict format without pypi key.\"\"\"\n    item = {\"local\": \"../foo\"}\n    result = _parse_local_dependency_item(item)\n    assert result == LocalDependency(local=\"../foo\", pypi=None)\n\n\ndef test_parse_local_dependency_item_invalid_dict() -> None:\n    \"\"\"Test parsing dict without local key raises error.\"\"\"\n    item = {\"pypi\": \"company-foo\"}\n    with pytest.raises(\n        ValueError,\n        match=\"Dictionary-style local dependency must have a 'local' key\",\n    ):\n        _parse_local_dependency_item(item)\n\n\ndef test_parse_local_dependency_item_invalid_type() -> None:\n    \"\"\"Test parsing invalid type raises error.\"\"\"\n    item = 123\n    with pytest.raises(TypeError, match=\"Invalid local dependency format\"):\n        _parse_local_dependency_item(item)  # type: ignore[arg-type]\n\n\ndef test_parse_local_dependency_item_invalid_use() -> None:\n    \"\"\"Invalid `use` value raises an error.\"\"\"\n    item = {\"local\": \"../foo\", \"use\": \"invalid\"}\n    with pytest.raises(ValueError, match=\"Invalid `use` value\"):\n        _parse_local_dependency_item(item)\n\n\ndef test_parse_local_dependency_item_use_pypi_requires_pypi() -> None:\n    \"\"\"`use: pypi` must provide a PyPI alternative.\"\"\"\n    item = {\"local\": \"../foo\", \"use\": \"pypi\"}\n    with pytest.raises(ValueError, match=\"must specify a `pypi` alternative\"):\n        _parse_local_dependency_item(item)\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_get_local_dependencies_mixed_format(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    \"\"\"Test parsing mixed string and dict format local dependencies.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True, parents=True)\n\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - ../foo\n                - local: ../bar\n                  pypi: company-bar\n                - local: ../baz\n                  pypi: company-baz\n                - ../qux\n            \"\"\",\n        ),\n    )\n    req_file = maybe_as_toml(toml_or_yaml, req_file)\n\n    # Load the file to get the data dict\n\n    yaml = YAML(typ=\"rt\")\n    with req_file.open() as f:\n        if req_file.suffix == \".toml\":\n            with req_file.open(\"rb\") as fb:\n                pyproject = tomllib.load(fb)\n                data = pyproject[\"tool\"][\"unidep\"]\n        else:\n            data = yaml.load(f)\n\n    local_deps = get_local_dependencies(data)\n\n    assert len(local_deps) == 4\n    assert local_deps[0] == LocalDependency(local=\"../foo\", pypi=None)\n    assert local_deps[1] == LocalDependency(local=\"../bar\", pypi=\"company-bar\")\n    assert local_deps[2] == LocalDependency(local=\"../baz\", pypi=\"company-baz\")\n    assert local_deps[3] == LocalDependency(local=\"../qux\", pypi=None)\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_setuptools_integration_with_pypi_alternatives(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,  # noqa: ARG001\n) -> None:\n    \"\"\"Test setuptools integration uses local paths when they exist.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True, parents=True)\n\n    # Create local dependency projects\n    foo = tmp_path / \"foo\"\n    foo.mkdir(exist_ok=True)\n    (foo / \"pyproject.toml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            [build-system]\n            requires = [\"setuptools\"]\n            build-backend = \"setuptools.build_meta\"\n\n            [project]\n            name = \"foo-pkg\"\n            version = \"0.1.0\"\n            \"\"\",\n        ),\n    )\n    # Create a Python module to make it a valid package\n    (foo / \"foo_pkg\").mkdir(exist_ok=True)\n    (foo / \"foo_pkg\" / \"__init__.py\").write_text(\"\")\n\n    bar = tmp_path / \"bar\"\n    bar.mkdir(exist_ok=True)\n    (bar / \"setup.py\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            from setuptools import setup\n            setup(name=\"bar-pkg\", version=\"0.1.0\")\n            \"\"\",\n        ),\n    )\n    # Create a Python module to make it a valid package\n    (bar / \"bar_pkg\").mkdir(exist_ok=True)\n    (bar / \"bar_pkg\" / \"__init__.py\").write_text(\"\")\n\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - ../foo\n                - local: ../bar\n                  pypi: company-bar\n            \"\"\",\n        ),\n    )\n    req_file = maybe_as_toml(toml_or_yaml, req_file)\n\n    # Test with local paths existing (development mode) - should use file:// URLs\n    deps = get_python_dependencies(\n        req_file,\n        include_local_dependencies=True,\n    )\n\n    assert \"numpy\" in deps.dependencies\n    # Both should use file:// URLs since local paths exist\n    assert any(\"foo-pkg @ file://\" in dep for dep in deps.dependencies)\n    assert any(\"bar-pkg @ file://\" in dep for dep in deps.dependencies)\n    # Should NOT use PyPI alternative when local exists\n    assert not any(\"company-bar\" in dep for dep in deps.dependencies)\n\n\ndef test_local_dependency_use_pypi_injects_dependency(tmp_path: Path) -> None:\n    \"\"\"`use: pypi` should add the PyPI requirement as a normal dependency.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir()\n    (project / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            \"\"\"\n            dependencies: []\n            local_dependencies:\n              - local: ./dep\n                pypi: company-dep>=1.0\n                use: pypi\n            \"\"\",\n        ),\n    )\n    (tmp_path / \"project\" / \"dep\").mkdir()\n\n    reqs = parse_requirements(project / \"requirements.yaml\")\n    assert \"company-dep\" in reqs.requirements\n    specs = reqs.requirements[\"company-dep\"]\n    assert specs[0].which == \"pip\"\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_standard_string_format(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    \"\"\"Test that standard string format for local dependencies works.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True, parents=True)\n\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - ../foo\n                - ../bar\n                - ../baz\n            \"\"\",\n        ),\n    )\n    req_file = maybe_as_toml(toml_or_yaml, req_file)\n\n    # This should work without errors\n    requirements = parse_requirements(req_file)\n    assert \"numpy\" in requirements.requirements\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_yaml_to_toml_with_pypi_alternatives(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    \"\"\"Test that yaml_to_toml preserves PyPI alternatives.\"\"\"\n    if toml_or_yaml == \"toml\":\n        # Skip for TOML as yaml_to_toml only works on YAML files\n        return\n\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True, parents=True)\n\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            name: test-project\n            dependencies:\n                - numpy\n            local_dependencies:\n                - ../foo\n                - local: ../bar\n                  pypi: company-bar\n            \"\"\",\n        ),\n    )\n\n    # Convert to TOML\n    toml_content = yaml_to_toml(req_file)\n\n    # Check that the structure is preserved\n    assert \"[tool.unidep]\" in toml_content\n    assert '\"../foo\"' in toml_content\n    assert '{ local = \"../bar\", pypi = \"company-bar\" }' in toml_content\n\n\ndef test_edge_cases(tmp_path: Path) -> None:  # noqa: ARG001\n    \"\"\"Test edge cases and error conditions.\"\"\"\n    # Test empty dict\n    with pytest.raises(\n        ValueError,\n        match=\"Dictionary-style local dependency must have a 'local' key\",\n    ):\n        _parse_local_dependency_item({})\n\n    # Test dict with only pypi key\n    with pytest.raises(\n        ValueError,\n        match=\"Dictionary-style local dependency must have a 'local' key\",\n    ):\n        _parse_local_dependency_item({\"pypi\": \"some-package\"})\n\n    # Test None value\n    with pytest.raises(TypeError, match=\"Invalid local dependency format\"):\n        _parse_local_dependency_item(None)  # type: ignore[arg-type]\n\n    # Test list value\n    with pytest.raises(TypeError, match=\"Invalid local dependency format\"):\n        _parse_local_dependency_item([\"foo\", \"bar\"])  # type: ignore[arg-type]\n\n\ndef test_local_dependency_with_extras(tmp_path: Path) -> None:\n    \"\"\"Test that local dependencies with extras work with PyPI alternatives.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True, parents=True)\n\n    # Create a local dependency with optional dependencies\n    dep = tmp_path / \"dep\"\n    dep.mkdir(exist_ok=True)\n    (dep / \"pyproject.toml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            [project]\n            name = \"my-dep\"\n            version = \"0.1.0\"\n\n            [tool.unidep]\n            dependencies = [\"requests\"]\n            optional_dependencies = {test = [\"pytest\"]}\n            \"\"\",\n        ),\n    )\n\n    # Main project references the local dependency with extras\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: ../dep[test]\n                  pypi: company-dep[test]\n            \"\"\",\n        ),\n    )\n\n    # Parse to ensure no errors\n    requirements = parse_requirements(req_file)\n    assert \"numpy\" in requirements.requirements\n\n\ndef test_recursive_local_dependencies_with_pypi_alternatives(tmp_path: Path) -> None:\n    \"\"\"Test that PyPI alternatives work with nested local dependencies.\"\"\"\n    # Create project structure: main -> dep1 -> dep2\n    main = tmp_path / \"main\"\n    main.mkdir(exist_ok=True)\n\n    dep1 = tmp_path / \"dep1\"\n    dep1.mkdir(exist_ok=True)\n\n    dep2 = tmp_path / \"dep2\"\n    dep2.mkdir(exist_ok=True)\n\n    # dep2 has no dependencies\n    (dep2 / \"requirements.yaml\").write_text(\"dependencies: [pandas]\")\n\n    # dep1 depends on dep2 with PyPI alternative\n    (dep1 / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: ../dep2\n                  pypi: company-dep2\n            \"\"\",\n        ),\n    )\n\n    # main depends on dep1 with PyPI alternative\n    (main / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - scipy\n            local_dependencies:\n                - local: ../dep1\n                  pypi: company-dep1\n            \"\"\",\n        ),\n    )\n\n    # Parse and check\n    requirements = parse_requirements(main / \"requirements.yaml\")\n    assert \"scipy\" in requirements.requirements\n    assert \"numpy\" in requirements.requirements  # From dep1\n    assert \"pandas\" in requirements.requirements  # From dep2\n\n\ndef test_empty_local_dependencies_list(tmp_path: Path) -> None:\n    \"\"\"Test handling of empty local_dependencies list.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies: []\n            \"\"\",\n        ),\n    )\n\n    # Test setuptools integration\n    deps = get_python_dependencies(\n        req_file,\n        include_local_dependencies=True,\n    )\n    assert \"numpy\" in deps.dependencies\n    assert len([d for d in deps.dependencies if \"file://\" in d]) == 0\n\n\ndef test_local_dependencies_with_extras(tmp_path: Path) -> None:\n    \"\"\"Test local dependencies with extras notation work with PyPI alternatives.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n\n    # Create a local dependency with optional dependencies\n    dep = tmp_path / \"dep\"\n    dep.mkdir(exist_ok=True)\n    (dep / \"pyproject.toml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            [build-system]\n            requires = [\"setuptools\", \"unidep\"]\n            build-backend = \"setuptools.build_meta\"\n\n            [project]\n            name = \"my-dep\"\n            version = \"0.1.0\"\n            dynamic = [\"dependencies\"]\n\n            [tool.unidep]\n            dependencies = [\"requests\"]\n            optional_dependencies = {test = [\"pytest\"], dev = [\"black\"]}\n            \"\"\",\n        ),\n    )\n    # Make it a valid package\n    (dep / \"my_dep\").mkdir(exist_ok=True)\n    (dep / \"my_dep\" / \"__init__.py\").write_text(\"\")\n\n    # Main project references the local dependency with extras\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: ../dep[test,dev]\n                  pypi: company-dep[test,dev]\n            \"\"\",\n        ),\n    )\n\n    # Test setuptools integration\n    deps = get_python_dependencies(\n        req_file,\n        include_local_dependencies=True,\n    )\n    assert \"numpy\" in deps.dependencies\n    # Should use file:// URL since local path exists\n    assert any(\"my-dep[test,dev] @ file://\" in dep for dep in deps.dependencies)\n    assert not any(\"company-dep\" in dep for dep in deps.dependencies)\n\n\ndef test_complex_path_structures(tmp_path: Path) -> None:\n    \"\"\"Test complex path structures including nested dirs and parent refs.\"\"\"\n    # Create complex directory structure\n    root = tmp_path / \"workspace\"\n    root.mkdir(exist_ok=True)\n\n    project = root / \"apps\" / \"main\"\n    project.mkdir(exist_ok=True, parents=True)\n\n    shared = root / \"libs\" / \"shared\"\n    shared.mkdir(exist_ok=True, parents=True)\n\n    utils = root / \"libs\" / \"utils\"\n    utils.mkdir(exist_ok=True, parents=True)\n\n    # Create valid packages\n    for pkg_dir, name in [(shared, \"shared\"), (utils, \"utils\")]:\n        (pkg_dir / \"setup.py\").write_text(\n            f'from setuptools import setup; setup(name=\"{name}\", version=\"1.0\")',\n        )\n        (pkg_dir / name).mkdir(exist_ok=True)\n        (pkg_dir / name / \"__init__.py\").write_text(\"\")\n\n    # Project with complex relative paths\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - pandas\n            local_dependencies:\n                - local: ../../libs/shared\n                  pypi: company-shared>=1.0\n                - local: ../../libs/utils\n                  pypi: company-utils~=2.0\n            \"\"\",\n        ),\n    )\n\n    # Test setuptools integration\n    deps = get_python_dependencies(\n        req_file,\n        include_local_dependencies=True,\n    )\n    assert \"pandas\" in deps.dependencies\n    # Should use file:// URLs since local paths exist\n    assert any(\"shared @ file://\" in dep for dep in deps.dependencies)\n    assert any(\"utils @ file://\" in dep for dep in deps.dependencies)\n    assert not any(\"company-shared\" in dep for dep in deps.dependencies)\n    assert not any(\"company-utils\" in dep for dep in deps.dependencies)\n\n\ndef test_invalid_yaml_handling(tmp_path: Path) -> None:\n    \"\"\"Test handling of invalid YAML in requirements file.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        \"\"\"\\\ndependencies:\n  - numpy\nlocal_dependencies:\n  - local: ../foo\n    pypi: company-foo\n  this is invalid yaml\n    - more invalid\n        \"\"\",\n    )\n\n    # Should raise an error when parsing\n\n    with pytest.raises((YAMLError, ValueError)):\n        parse_requirements(req_file)\n\n\ndef test_pypi_alternatives_with_absolute_paths(tmp_path: Path) -> None:\n    \"\"\"Test that absolute paths in local dependencies are handled correctly.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n\n    # Create a dependency with absolute path\n    dep = tmp_path / \"absolute_dep\"\n    dep.mkdir(exist_ok=True)\n    (dep / \"setup.py\").write_text(\n        'from setuptools import setup; setup(name=\"abs-dep\", version=\"1.0\")',\n    )\n    (dep / \"abs_dep\").mkdir(exist_ok=True)\n    (dep / \"abs_dep\" / \"__init__.py\").write_text(\"\")\n\n    req_file = project / \"requirements.yaml\"\n    # Note: Using absolute path to trigger the assertion\n    abs_path = str(dep.resolve())\n    req_file.write_text(\n        textwrap.dedent(\n            f\"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: {abs_path}\n                  pypi: company-abs-dep\n            \"\"\",\n        ),\n    )\n\n    # This should fail because absolute paths are not allowed\n\n    with pytest.raises(AssertionError):\n        parse_local_dependencies(req_file)\n\n\ndef test_pypi_alternatives_when_local_missing(tmp_path: Path) -> None:\n    \"\"\"Test that PyPI alternatives are used when local paths don't exist.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - ../missing1\n                - local: ../missing2\n                  pypi: company-missing2\n            \"\"\",\n        ),\n    )\n\n    # Test with missing local paths - should use PyPI alternatives when available\n    deps = get_python_dependencies(\n        req_file,\n        include_local_dependencies=True,\n    )\n\n    assert \"numpy\" in deps.dependencies\n    # missing1 has no PyPI alternative and doesn't exist - should be skipped\n    assert not any(\"missing1\" in dep for dep in deps.dependencies)\n    # missing2 should use PyPI alternative since local doesn't exist\n    assert any(\"company-missing2\" in dep for dep in deps.dependencies)\n    # Should NOT have file:// URLs for missing paths\n    assert not any(\"file://\" in dep for dep in deps.dependencies)\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_mixed_string_and_dict_in_toml(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    \"\"\"Test that mixed string and dict formats work in TOML.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n\n    # Create dependencies\n    for name in [\"dep1\", \"dep2\", \"dep3\"]:\n        dep = tmp_path / name\n        dep.mkdir(exist_ok=True)\n        (dep / \"setup.py\").write_text(\n            f'from setuptools import setup; setup(name=\"{name}\", version=\"1.0\")',\n        )\n        (dep / name).mkdir(exist_ok=True)\n        (dep / name / \"__init__.py\").write_text(\"\")\n\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - ../dep1\n                - local: ../dep2\n                  pypi: company-dep2\n                - local: ../dep3\n            \"\"\",\n        ),\n    )\n    req_file = maybe_as_toml(toml_or_yaml, req_file)\n\n    # Test parsing\n    requirements = parse_requirements(req_file)\n    assert \"numpy\" in requirements.requirements\n\n\ndef test_wheel_file_with_pypi_alternatives(tmp_path: Path) -> None:\n    \"\"\"Test handling of .whl files with PyPI alternatives.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir()\n\n    # Test 1: Wheel exists - should use it\n    wheel_path = tmp_path / \"dep.whl\"\n    wheel_path.touch()  # Create dummy wheel file\n\n    (project / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            f\"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: {wheel_path}\n                  pypi: company-dep>=1.0\n            \"\"\",\n        ),\n    )\n\n    deps = get_python_dependencies(\n        project / \"requirements.yaml\",\n        include_local_dependencies=True,\n    )\n\n    assert \"numpy\" in deps.dependencies\n    assert any(\"dep.whl @ file://\" in dep for dep in deps.dependencies)\n    assert not any(\"company-dep\" in dep for dep in deps.dependencies)\n\n    # Test 2: Wheel doesn't exist - should use PyPI alternative\n    wheel_path.unlink()  # Remove wheel file\n\n    deps = get_python_dependencies(\n        project / \"requirements.yaml\",\n        include_local_dependencies=True,\n    )\n\n    assert \"numpy\" in deps.dependencies\n    assert \"company-dep>=1.0\" in deps.dependencies\n    assert not any(\"file://\" in dep for dep in deps.dependencies)\n\n    # Test 3: Wheel with UNIDEP_SKIP_LOCAL_DEPS - should use PyPI\n    wheel_path.touch()  # Recreate wheel\n\n    deps = get_python_dependencies(\n        project / \"requirements.yaml\",\n        include_local_dependencies=False,  # UNIDEP_SKIP_LOCAL_DEPS=1\n    )\n\n    assert \"numpy\" in deps.dependencies\n    assert \"company-dep>=1.0\" in deps.dependencies\n    assert not any(\"file://\" in dep for dep in deps.dependencies)\n\n\ndef test_skip_local_deps_with_pypi_alternatives(tmp_path: Path) -> None:\n    \"\"\"Test that UNIDEP_SKIP_LOCAL_DEPS uses PyPI alternatives when available.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir()\n\n    # Create local dependencies\n    dep1 = tmp_path / \"dep1\"\n    dep1.mkdir()\n    (dep1 / \"setup.py\").write_text(\n        'from setuptools import setup; setup(name=\"dep1-local\", version=\"0.1.0\")',\n    )\n\n    dep2 = tmp_path / \"dep2\"\n    dep2.mkdir()\n    (dep2 / \"setup.py\").write_text(\n        'from setuptools import setup; setup(name=\"dep2-local\", version=\"0.1.0\")',\n    )\n\n    # Create project with mixed local dependencies\n    (project / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - ../dep1  # String format - no PyPI alternative\n                - local: ../dep2\n                  pypi: company-dep2>=1.0  # Has PyPI alternative\n            \"\"\",\n        ),\n    )\n\n    # Test with include_local_dependencies=False (UNIDEP_SKIP_LOCAL_DEPS=1)\n    deps = get_python_dependencies(\n        project / \"requirements.yaml\",\n        include_local_dependencies=False,\n    )\n\n    # Check results\n    assert \"numpy\" in deps.dependencies\n    # dep1 should be completely skipped (no PyPI alternative)\n    assert not any(\"dep1\" in dep for dep in deps.dependencies)\n    # dep2 should use PyPI alternative\n    assert \"company-dep2>=1.0\" in deps.dependencies\n    # No file:// URLs should be present\n    assert not any(\"file://\" in dep for dep in deps.dependencies)\n\n\ndef test_regular_local_deps_with_existing_paths(tmp_path: Path) -> None:\n    \"\"\"Test regular (non-wheel) local dependencies that exist and are pip-installable.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir()\n\n    # Create local dependency with different package structures\n    # Test 1: pyproject.toml\n    dep1 = tmp_path / \"dep1\"\n    dep1.mkdir()\n    (dep1 / \"pyproject.toml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            [build-system]\n            requires = [\"setuptools\"]\n            build-backend = \"setuptools.build_meta\"\n\n            [project]\n            name = \"my-dep1\"\n            version = \"0.1.0\"\n            \"\"\",\n        ),\n    )\n    (dep1 / \"my_dep1\").mkdir()\n    (dep1 / \"my_dep1\" / \"__init__.py\").write_text(\"\")\n\n    # Test 2: setup.cfg\n    dep2 = tmp_path / \"dep2\"\n    dep2.mkdir()\n    (dep2 / \"setup.cfg\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            [metadata]\n            name = my-dep2\n            version = 0.1.0\n            \"\"\",\n        ),\n    )\n    (dep2 / \"setup.py\").write_text(\"from setuptools import setup; setup()\")\n    (dep2 / \"my_dep2\").mkdir()\n    (dep2 / \"my_dep2\" / \"__init__.py\").write_text(\"\")\n\n    # Test 3: setup.py\n    dep3 = tmp_path / \"dep3\"\n    dep3.mkdir()\n    (dep3 / \"setup.py\").write_text(\n        'from setuptools import setup; setup(name=\"my-dep3\", version=\"0.1.0\")',\n    )\n    (dep3 / \"my_dep3\").mkdir()\n    (dep3 / \"my_dep3\" / \"__init__.py\").write_text(\"\")\n\n    # Project with PyPI alternatives\n    (project / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: ../dep1\n                  pypi: company-dep1>=1.0\n                - local: ../dep2\n                  pypi: company-dep2>=2.0\n                - local: ../dep3\n                  pypi: company-dep3>=3.0\n            \"\"\",\n        ),\n    )\n\n    # Test with local paths existing\n    deps = get_python_dependencies(\n        project / \"requirements.yaml\",\n        include_local_dependencies=True,\n    )\n\n    # All should use file:// URLs since local paths exist\n    assert \"numpy\" in deps.dependencies\n    assert any(\"my-dep1 @ file://\" in dep for dep in deps.dependencies)\n    assert any(\"my-dep2 @ file://\" in dep for dep in deps.dependencies)\n    assert any(\"my-dep3 @ file://\" in dep for dep in deps.dependencies)\n    # Should NOT use PyPI alternatives\n    assert not any(\"company-dep\" in dep for dep in deps.dependencies)\n\n\ndef test_local_deps_with_extras_and_pypi_alternatives(tmp_path: Path) -> None:\n    \"\"\"Test local dependencies with extras notation and PyPI alternatives.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir()\n\n    # Create dependency with extras\n    dep = tmp_path / \"dep\"\n    dep.mkdir()\n    (dep / \"pyproject.toml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            [build-system]\n            requires = [\"setuptools\"]\n            build-backend = \"setuptools.build_meta\"\n\n            [project]\n            name = \"my-dep-extras\"\n            version = \"0.1.0\"\n            dependencies = [\"requests\"]\n\n            [project.optional-dependencies]\n            test = [\"pytest\"]\n            dev = [\"black\", \"ruff\"]\n            \"\"\",\n        ),\n    )\n    (dep / \"my_dep_extras\").mkdir()\n    (dep / \"my_dep_extras\" / \"__init__.py\").write_text(\"\")\n\n    # Test various extras notations\n    (project / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: ../dep[test]\n                  pypi: company-dep[test]>=1.0\n                - local: ../dep[dev]\n                  pypi: company-dep[dev]>=1.0\n                - local: ../dep[test,dev]\n                  pypi: company-dep[test,dev]>=1.0\n            \"\"\",\n        ),\n    )\n\n    # Test with local paths existing\n    deps = get_python_dependencies(\n        project / \"requirements.yaml\",\n        include_local_dependencies=True,\n    )\n\n    # Should use file:// URLs with extras preserved\n    assert \"numpy\" in deps.dependencies\n    assert any(\"my-dep-extras[test] @ file://\" in dep for dep in deps.dependencies)\n    assert any(\"my-dep-extras[dev] @ file://\" in dep for dep in deps.dependencies)\n    assert any(\"my-dep-extras[test,dev] @ file://\" in dep for dep in deps.dependencies)\n    # Should NOT use PyPI alternatives\n    assert not any(\"company-dep\" in dep for dep in deps.dependencies)\n\n\ndef test_local_deps_missing_with_pypi_fallback(tmp_path: Path) -> None:\n    \"\"\"Test regular local dependencies that don't exist fall back to PyPI alternatives.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir()\n\n    # Create project with non-existent local dependencies\n    (project / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: ../missing-dep1\n                  pypi: company-dep1>=1.0\n                - local: ../missing-dep2[extras]\n                  pypi: company-dep2[extras]>=2.0\n                - ../missing-dep3  # No PyPI alternative\n            \"\"\",\n        ),\n    )\n\n    # Test with missing local paths\n    deps = get_python_dependencies(\n        project / \"requirements.yaml\",\n        include_local_dependencies=True,\n    )\n\n    # Should use PyPI alternatives when available\n    assert \"numpy\" in deps.dependencies\n    assert \"company-dep1>=1.0\" in deps.dependencies\n    assert \"company-dep2[extras]>=2.0\" in deps.dependencies\n    # missing-dep3 should be skipped (no PyPI alternative)\n    assert not any(\"missing-dep3\" in dep for dep in deps.dependencies)\n    # No file:// URLs since paths don't exist\n    assert not any(\"file://\" in dep for dep in deps.dependencies)\n\n\ndef test_missing_requirements_file_handling(tmp_path: Path) -> None:\n    \"\"\"Test handling when requirements.yaml doesn't exist.\"\"\"\n    # Test 1: raises_if_missing=True (default) - should raise\n    with pytest.raises(FileNotFoundError):\n        get_python_dependencies(\n            tmp_path / \"non_existent.yaml\",\n            raises_if_missing=True,\n        )\n\n    # Test 2: raises_if_missing=False - should return empty\n    deps = get_python_dependencies(\n        tmp_path / \"non_existent.yaml\",\n        raises_if_missing=False,\n    )\n    assert deps.dependencies == []\n    assert deps.extras == {}\n\n\ndef test_package_name_extraction_edge_cases(tmp_path: Path) -> None:\n    \"\"\"Test edge cases for package name extraction from various file formats.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir()\n\n    # Test 1: setup.cfg without name\n    dep1 = tmp_path / \"dep1\"\n    dep1.mkdir()\n    (dep1 / \"setup.cfg\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            [metadata]\n            version = 0.1.0\n            # Missing name field\n            \"\"\",\n        ),\n    )\n    (dep1 / \"setup.py\").write_text(\"from setuptools import setup; setup()\")\n    (dep1 / \"dep1\").mkdir()\n    (dep1 / \"dep1\" / \"__init__.py\").write_text(\"\")\n\n    # Test 2: setup.py without name\n    dep2 = tmp_path / \"dep2\"\n    dep2.mkdir()\n    (dep2 / \"setup.py\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            from setuptools import setup\n            setup(version=\"0.1.0\")  # Missing name\n            \"\"\",\n        ),\n    )\n    (dep2 / \"dep2\").mkdir()\n    (dep2 / \"dep2\" / \"__init__.py\").write_text(\"\")\n\n    # Test 3: pyproject.toml with Poetry format\n    dep3 = tmp_path / \"dep3\"\n    dep3.mkdir()\n    (dep3 / \"pyproject.toml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            [build-system]\n            requires = [\"poetry-core\"]\n            build-backend = \"poetry.core.masonry.api\"\n\n            [tool.poetry]\n            name = \"poetry-dep\"\n            version = \"0.1.0\"\n            \"\"\",\n        ),\n    )\n    (dep3 / \"poetry_dep\").mkdir()\n    (dep3 / \"poetry_dep\" / \"__init__.py\").write_text(\"\")\n\n    # Test 4: pyproject.toml without name anywhere\n    dep4 = tmp_path / \"dep4\"\n    dep4.mkdir()\n    (dep4 / \"pyproject.toml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            [build-system]\n            requires = [\"setuptools\"]\n            build-backend = \"setuptools.build_meta\"\n\n            # No project section, no name anywhere\n            [tool.setuptools]\n            packages = [\"dep4\"]\n            \"\"\",\n        ),\n    )\n    (dep4 / \"dep4\").mkdir()\n    (dep4 / \"dep4\" / \"__init__.py\").write_text(\"\")\n\n    # Test 5: Minimal setup.py - fallback to folder name\n    dep5 = tmp_path / \"folder-name-dep\"\n    dep5.mkdir()\n    # Minimal setup.py to make it pip-installable\n    (dep5 / \"setup.py\").write_text(\"from setuptools import setup; setup()\")\n    (dep5 / \"folder_name_dep\").mkdir()\n    (dep5 / \"folder_name_dep\" / \"__init__.py\").write_text(\"\")\n\n    # Create project referencing these deps\n    (project / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: ../dep1\n                  pypi: company-dep1\n                - local: ../dep2\n                  pypi: company-dep2\n                - local: ../dep3\n                  pypi: company-dep3\n                - local: ../dep4\n                  pypi: company-dep4\n                - local: ../folder-name-dep\n                  pypi: company-dep5\n            \"\"\",\n        ),\n    )\n\n    # Test with local paths existing\n    deps = get_python_dependencies(\n        project / \"requirements.yaml\",\n        include_local_dependencies=True,\n    )\n\n    # Check that all dependencies were processed\n    assert \"numpy\" in deps.dependencies\n    # dep1: falls back to folder name \"dep1\"\n    assert any(\"dep1 @ file://\" in dep for dep in deps.dependencies)\n    # dep2: falls back to folder name \"dep2\"\n    assert any(\"dep2 @ file://\" in dep for dep in deps.dependencies)\n    # dep3: uses poetry name \"poetry-dep\"\n    assert any(\"poetry-dep @ file://\" in dep for dep in deps.dependencies)\n    # dep4: falls back to folder name \"dep4\"\n    assert any(\"dep4 @ file://\" in dep for dep in deps.dependencies)\n    # dep5: uses folder name \"folder-name-dep\"\n    assert any(\"folder-name-dep @ file://\" in dep for dep in deps.dependencies)\n"
  },
  {
    "path": "tests/test_pypi_alternatives_errors.py",
    "content": "\"\"\"Test error cases and special scenarios for PyPI alternatives.\"\"\"\n\nfrom __future__ import annotations\n\nimport textwrap\nfrom typing import TYPE_CHECKING\n\nimport pytest\n\nfrom unidep import parse_local_dependencies\nfrom unidep._dependencies_parsing import parse_requirements\nfrom unidep._setuptools_integration import get_python_dependencies\n\nif TYPE_CHECKING:\n    from pathlib import Path\n\n\ndef test_local_dependency_wheel_with_pypi_alternative(tmp_path: Path) -> None:\n    \"\"\"Test that wheel files work with PyPI alternatives.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n\n    # Create a dummy wheel file\n    wheel_file = tmp_path / \"some_package.whl\"\n    wheel_file.write_text(\"dummy wheel content\")\n\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: ../some_package.whl\n                  pypi: company-package==1.0.0\n            \"\"\",\n        ),\n    )\n\n    # This should work without errors\n    requirements = parse_requirements(req_file)\n    assert \"numpy\" in requirements.requirements\n\n    # The wheel should be handled in parse_local_dependencies\n\n    deps = parse_local_dependencies(req_file, verbose=True)\n    assert len(deps) == 1\n    # Get the first (and only) list of paths\n    paths = next(iter(deps.values()))\n    assert len(paths) == 1\n    # Compare resolved paths to handle Windows path differences\n    assert paths[0].resolve() == wheel_file.resolve()\n\n\ndef test_missing_local_dependency_with_pypi_alternative(tmp_path: Path) -> None:\n    \"\"\"Test behavior when local dependency doesn't exist but has PyPI alternative.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: ../missing_dep\n                  pypi: company-missing\n            \"\"\",\n        ),\n    )\n\n    # Should not raise when raise_if_missing=False\n\n    deps = parse_local_dependencies(req_file, raise_if_missing=False)\n    assert len(deps) == 0\n\n    # Should raise when raise_if_missing=True\n    with pytest.raises(FileNotFoundError):\n        parse_local_dependencies(req_file, raise_if_missing=True)\n\n\ndef test_empty_folder_with_pypi_alternative(tmp_path: Path) -> None:\n    \"\"\"Test error when local dependency is an empty folder.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n\n    # Create empty folder\n    empty_dep = tmp_path / \"empty_dep\"\n    empty_dep.mkdir(exist_ok=True)\n\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: ../empty_dep\n                  pypi: company-empty\n            \"\"\",\n        ),\n    )\n\n    # Should raise RuntimeError for empty folder\n\n    with pytest.raises(\n        RuntimeError,\n        match=\"is not pip installable because it is an empty folder\",\n    ):\n        parse_local_dependencies(req_file)\n\n\ndef test_empty_git_submodule_with_pypi_alternative(tmp_path: Path) -> None:\n    \"\"\"Test error when local dependency is an empty git submodule.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n\n    # Create a directory that looks like an empty git submodule\n    git_submodule = tmp_path / \"git_submodule\"\n    git_submodule.mkdir(exist_ok=True)\n    (git_submodule / \".git\").write_text(\"gitdir: ../.git/modules/git_submodule\")\n\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: ../git_submodule\n                  pypi: company-submodule\n            \"\"\",\n        ),\n    )\n\n    # Should raise RuntimeError for empty git submodule\n\n    with pytest.raises(\n        RuntimeError,\n        match=\"is not installable by pip because it is an empty Git submodule\",\n    ):\n        parse_local_dependencies(req_file)\n\n\ndef test_non_pip_installable_with_pypi_alternative(tmp_path: Path) -> None:\n    \"\"\"Test error when local dependency is not pip installable.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n\n    # Create a non-pip-installable directory (no setup.py, pyproject.toml, etc.)\n    non_pip = tmp_path / \"non_pip\"\n    non_pip.mkdir(exist_ok=True)\n    (non_pip / \"some_file.txt\").write_text(\"not a python package\")\n\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: ../non_pip\n                  pypi: company-non-pip\n            \"\"\",\n        ),\n    )\n\n    # Should raise RuntimeError\n\n    with pytest.raises(\n        RuntimeError,\n        match=\"is not pip installable nor is it managed by unidep\",\n    ):\n        parse_local_dependencies(req_file)\n\n\ndef test_circular_dependencies_with_pypi_alternatives(tmp_path: Path) -> None:\n    \"\"\"Test circular dependencies with PyPI alternatives.\"\"\"\n    project1 = tmp_path / \"project1\"\n    project1.mkdir(exist_ok=True)\n\n    project2 = tmp_path / \"project2\"\n    project2.mkdir(exist_ok=True)\n\n    # project1 depends on project2\n    (project1 / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - pandas\n            local_dependencies:\n                - local: ../project2\n                  pypi: company-project2\n            \"\"\",\n        ),\n    )\n\n    # project2 depends on project1 (circular)\n    (project2 / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: ../project1\n                  pypi: company-project1\n            \"\"\",\n        ),\n    )\n\n    # Should handle circular dependencies gracefully\n    requirements = parse_requirements(\n        project1 / \"requirements.yaml\",\n        project2 / \"requirements.yaml\",\n    )\n    assert \"pandas\" in requirements.requirements\n    assert \"numpy\" in requirements.requirements\n\n\ndef test_very_long_pypi_alternative_names(tmp_path: Path) -> None:\n    \"\"\"Test handling of very long PyPI package names in alternatives.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n\n    # Create a local dependency\n    dep = tmp_path / \"dep\"\n    dep.mkdir(exist_ok=True)\n    (dep / \"setup.py\").write_text(\n        'from setuptools import setup; setup(name=\"dep\", version=\"1.0\")',\n    )\n    (dep / \"dep\").mkdir(exist_ok=True)\n    (dep / \"dep\" / \"__init__.py\").write_text(\"\")\n\n    # Very long PyPI alternative name\n    long_name = \"company-\" + \"x\" * 200 + \"-package>=1.0.0\"\n\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            f\"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: ../dep\n                  pypi: {long_name}\n            \"\"\",\n        ),\n    )\n\n    # Should handle long names without issues\n\n    # Test with local path existing - should use file:// URL\n    deps = get_python_dependencies(\n        req_file,\n        include_local_dependencies=True,\n    )\n    assert \"numpy\" in deps.dependencies\n    assert any(\"dep @ file://\" in d for d in deps.dependencies)\n\n    # Test with local path missing - should use PyPI alternative\n    import shutil\n\n    shutil.rmtree(dep)\n\n    deps = get_python_dependencies(\n        req_file,\n        include_local_dependencies=True,\n    )\n    assert \"numpy\" in deps.dependencies\n    assert long_name in deps.dependencies\n\n\ndef test_special_characters_in_paths(tmp_path: Path) -> None:\n    \"\"\"Test handling of special characters in local dependency paths.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n\n    # Create a dependency with special characters in name\n    special_dir = tmp_path / \"dep with spaces & special-chars\"\n    special_dir.mkdir(exist_ok=True)\n    (special_dir / \"setup.py\").write_text(\n        'from setuptools import setup; setup(name=\"special-dep\", version=\"1.0\")',\n    )\n    (special_dir / \"special_dep\").mkdir(exist_ok=True)\n    (special_dir / \"special_dep\" / \"__init__.py\").write_text(\"\")\n\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: \"../dep with spaces & special-chars\"\n                  pypi: company-special-dep\n            \"\"\",\n        ),\n    )\n\n    # Should handle special characters correctly\n\n    # With local path existing - should use file:// URL\n    deps = get_python_dependencies(\n        req_file,\n        include_local_dependencies=True,\n    )\n    assert \"numpy\" in deps.dependencies\n    assert any(\"special-dep @ file://\" in d for d in deps.dependencies)\n    assert not any(\"company-special-dep\" in d for d in deps.dependencies)\n\n\ndef test_symlink_local_dependencies(tmp_path: Path) -> None:\n    \"\"\"Test handling of symlinked local dependencies.\"\"\"\n    import os\n\n    # Skip on Windows where symlinks require admin privileges\n    if os.name == \"nt\":\n        pytest.skip(\"Symlink test skipped on Windows\")\n\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n\n    # Create actual dependency\n    actual_dep = tmp_path / \"actual_dep\"\n    actual_dep.mkdir(exist_ok=True)\n    (actual_dep / \"setup.py\").write_text(\n        'from setuptools import setup; setup(name=\"actual\", version=\"1.0\")',\n    )\n    (actual_dep / \"actual\").mkdir(exist_ok=True)\n    (actual_dep / \"actual\" / \"__init__.py\").write_text(\"\")\n\n    # Create symlink\n    symlink_dep = tmp_path / \"symlink_dep\"\n    symlink_dep.symlink_to(actual_dep)\n\n    req_file = project / \"requirements.yaml\"\n    req_file.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: ../symlink_dep\n                  pypi: company-symlink-dep\n            \"\"\",\n        ),\n    )\n\n    # Should resolve symlinks correctly\n\n    # With symlink existing - should use file:// URL\n    deps = get_python_dependencies(\n        req_file,\n        include_local_dependencies=True,\n    )\n    assert \"numpy\" in deps.dependencies\n    assert any(\"actual @ file://\" in d for d in deps.dependencies)\n    assert not any(\"company-symlink-dep\" in d for d in deps.dependencies)\n"
  },
  {
    "path": "tests/test_pypi_alternatives_integration.py",
    "content": "\"\"\"Integration tests for PyPI alternatives in local dependencies.\"\"\"\n\nfrom __future__ import annotations\n\nimport shutil\nimport textwrap\nfrom typing import TYPE_CHECKING\n\nfrom unidep._setuptools_integration import get_python_dependencies\n\nif TYPE_CHECKING:\n    from pathlib import Path\n\n    import pytest\n\n\ndef test_build_with_pypi_alternatives(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Test that building a wheel uses PyPI alternatives when local paths don't exist.\"\"\"\n    # Create main project\n    project = tmp_path / \"main_project\"\n    project.mkdir(exist_ok=True)\n\n    # Create local dependency\n    local_dep = tmp_path / \"local_dep\"\n    local_dep.mkdir(exist_ok=True)\n    (local_dep / \"pyproject.toml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            [build-system]\n            requires = [\"setuptools\", \"unidep\"]\n            build-backend = \"setuptools.build_meta\"\n\n            [project]\n            name = \"local-dep\"\n            version = \"0.1.0\"\n\n            [tool.unidep]\n            dependencies = [\"requests\"]\n            \"\"\",\n        ),\n    )\n    (local_dep / \"local_dep.py\").write_text(\"# Local dependency module\")\n\n    # Create main project with PyPI alternative\n    (project / \"pyproject.toml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            [build-system]\n            requires = [\"setuptools\", \"unidep\"]\n            build-backend = \"setuptools.build_meta\"\n\n            [project]\n            name = \"main-project\"\n            version = \"0.1.0\"\n            dynamic = [\"dependencies\"]\n\n            [tool.unidep]\n            dependencies = [\"numpy\"]\n            local_dependencies = [\n                {local = \"../local_dep\", pypi = \"company-local-dep==1.0.0\"}\n            ]\n            \"\"\",\n        ),\n    )\n    (project / \"main_project.py\").write_text(\"# Main project module\")\n\n    # Change to project directory\n    monkeypatch.chdir(project)\n\n    # Test 1: Normal development with local paths existing - should use file:// URLs\n\n    deps = get_python_dependencies(\n        project / \"pyproject.toml\",\n        include_local_dependencies=True,\n    )\n\n    assert \"numpy\" in deps.dependencies\n    # Should use file:// URL since local path exists\n    assert any(\"local-dep @ file://\" in dep for dep in deps.dependencies)\n    assert not any(\"company-local-dep\" in dep for dep in deps.dependencies)\n\n    # Test 2: Simulate wheel build where local paths don't exist\n    # Move the local dependency to simulate it not being available\n\n    local_dep_backup = tmp_path / \"local_dep_backup\"\n    shutil.move(str(local_dep), str(local_dep_backup))\n\n    deps = get_python_dependencies(\n        project / \"pyproject.toml\",\n        include_local_dependencies=True,\n    )\n\n    assert \"numpy\" in deps.dependencies\n    # Should use PyPI alternative since local path doesn't exist\n    assert \"company-local-dep==1.0.0\" in deps.dependencies\n    assert not any(\"file://\" in dep for dep in deps.dependencies)\n\n\ndef test_mixed_local_deps_with_and_without_pypi(tmp_path: Path) -> None:\n    \"\"\"Test project with some local deps having PyPI alternatives and some not.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n\n    # Create local dependencies\n    for name in [\"dep1\", \"dep2\", \"dep3\"]:\n        dep_dir = tmp_path / name\n        dep_dir.mkdir(exist_ok=True)\n        (dep_dir / \"setup.py\").write_text(\n            f'from setuptools import setup; setup(name=\"{name}\", version=\"0.1.0\")',\n        )\n\n    # Create requirements.yaml with mixed format\n    (project / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - pandas\n            local_dependencies:\n                - ../dep1  # No PyPI alternative\n                - local: ../dep2\n                  pypi: company-dep2>=2.0\n                - local: ../dep3\n                  pypi: company-dep3~=3.0\n            \"\"\",\n        ),\n    )\n\n    deps = get_python_dependencies(\n        project / \"requirements.yaml\",\n        include_local_dependencies=True,\n    )\n\n    assert \"pandas\" in deps.dependencies\n    # All should use file:// since local paths exist\n    assert any(\"dep1 @ file://\" in dep for dep in deps.dependencies)\n    assert any(\"dep2 @ file://\" in dep for dep in deps.dependencies)\n    assert any(\"dep3 @ file://\" in dep for dep in deps.dependencies)\n    # Should NOT use PyPI alternatives when local exists\n    assert not any(\"company-dep2\" in dep for dep in deps.dependencies)\n    assert not any(\"company-dep3\" in dep for dep in deps.dependencies)\n\n\ndef test_setuptools_with_skip_local_deps_env_var(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Test that UNIDEP_SKIP_LOCAL_DEPS environment variable behavior.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n\n    # Create local dependency\n    dep = tmp_path / \"dep\"\n    dep.mkdir(exist_ok=True)\n    (dep / \"setup.py\").write_text(\n        'from setuptools import setup; setup(name=\"my-dep\", version=\"0.1.0\")',\n    )\n\n    # Create project with local dependency (no PyPI alternative)\n    (project / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - ../dep  # No PyPI alternative\n            \"\"\",\n        ),\n    )\n\n    # Test without UNIDEP_SKIP_LOCAL_DEPS\n\n    deps = get_python_dependencies(\n        project / \"requirements.yaml\",\n        include_local_dependencies=True,\n    )\n\n    assert \"numpy\" in deps.dependencies\n    assert any(\"my-dep @ file://\" in dep for dep in deps.dependencies)\n\n    # Test with UNIDEP_SKIP_LOCAL_DEPS=1\n    monkeypatch.setenv(\"UNIDEP_SKIP_LOCAL_DEPS\", \"1\")\n\n    deps = get_python_dependencies(\n        project / \"requirements.yaml\",\n        include_local_dependencies=False,  # This would be set by _deps()\n    )\n\n    assert \"numpy\" in deps.dependencies\n    # Should not include local dependency\n    assert not any(\"my-dep\" in dep for dep in deps.dependencies)\n    assert not any(\"file://\" in dep for dep in deps.dependencies)\n\n\ndef test_use_skip_entries_are_ignored(tmp_path: Path) -> None:\n    \"\"\"Entries marked `use: skip` should never contribute dependencies.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n    skip_dep = tmp_path / \"skip_dep\"\n    skip_dep.mkdir(exist_ok=True)\n    (skip_dep / \"setup.py\").write_text(\n        'from setuptools import setup; setup(name=\"skip-dep\", version=\"0.1.0\")',\n    )\n    (project / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: ../skip_dep\n                  use: skip\n            \"\"\",\n        ),\n    )\n\n    deps = get_python_dependencies(\n        project / \"requirements.yaml\",\n        include_local_dependencies=True,\n    )\n\n    assert \"numpy\" in deps.dependencies\n    assert not any(\"skip-dep\" in dep for dep in deps.dependencies)\n    assert not any(\"file://\" in dep for dep in deps.dependencies)\n\n\ndef test_use_pypi_entries_not_readded(tmp_path: Path) -> None:\n    \"\"\"Entries marked `use: pypi` rely solely on their PyPI alternative.\"\"\"\n    project = tmp_path / \"project\"\n    project.mkdir(exist_ok=True)\n    local_dep = tmp_path / \"pypi_dep\"\n    local_dep.mkdir(exist_ok=True)\n    (local_dep / \"setup.py\").write_text(\n        'from setuptools import setup; setup(name=\"pypi-dep\", version=\"0.1.0\")',\n    )\n    (project / \"requirements.yaml\").write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numpy\n            local_dependencies:\n                - local: ../pypi_dep\n                  use: pypi\n                  pypi: company-pypi-dep==2.0\n            \"\"\",\n        ),\n    )\n\n    deps = get_python_dependencies(\n        project / \"requirements.yaml\",\n        include_local_dependencies=True,\n    )\n\n    assert \"numpy\" in deps.dependencies\n    assert any(\n        dep.replace(\" \", \"\") == \"company-pypi-dep==2.0\" for dep in deps.dependencies\n    )\n    assert not any(\"pypi-dep @ file://\" in dep for dep in deps.dependencies)\n"
  },
  {
    "path": "tests/test_setuptools_integration.py",
    "content": "\"\"\"Tests for setuptools integration.\"\"\"\n\nimport textwrap\nfrom pathlib import Path\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom unidep._setuptools_integration import filter_python_dependencies\nfrom unidep.utils import (\n    package_name_from_path,\n    package_name_from_pyproject_toml,\n    package_name_from_setup_cfg,\n    package_name_from_setup_py,\n)\n\nREPO_ROOT = Path(__file__).parent.parent\n\n\ndef test_package_name_from_path() -> None:\n    example = REPO_ROOT / \"example\"\n    # Could not find the package name, so it uses the folder name\n    assert package_name_from_path(example) == \"example\"\n    # The following should read from the setup.py or pyproject.toml file\n    assert package_name_from_path(example / \"hatch_project\") == \"hatch_project\"\n    assert (\n        package_name_from_pyproject_toml(example / \"hatch_project\" / \"pyproject.toml\")\n        == \"hatch_project\"\n    )\n    assert package_name_from_path(example / \"hatch2_project\") == \"hatch2_project\"\n    assert (\n        package_name_from_pyproject_toml(example / \"hatch2_project\" / \"pyproject.toml\")\n        == \"hatch2_project\"\n    )\n    assert (\n        package_name_from_path(example / \"pyproject_toml_project\")\n        == \"pyproject_toml_project\"\n    )\n    assert (\n        package_name_from_pyproject_toml(\n            example / \"pyproject_toml_project\" / \"pyproject.toml\",\n        )\n        == \"pyproject_toml_project\"\n    )\n    assert package_name_from_path(example / \"setup_py_project\") == \"setup_py_project\"\n    assert (\n        package_name_from_setup_py(example / \"setup_py_project\" / \"setup.py\")\n        == \"setup_py_project\"\n    )\n    assert (\n        package_name_from_path(example / \"setuptools_project\") == \"setuptools_project\"\n    )\n    assert (\n        package_name_from_pyproject_toml(\n            example / \"setuptools_project\" / \"pyproject.toml\",\n        )\n        == \"setuptools_project\"\n    )\n\n\ndef test_package_name_from_cfg(tmp_path: Path) -> None:\n    setup_cfg = tmp_path / \"setup.cfg\"\n    setup_cfg.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            [metadata]\n            name = setup_cfg_project\n            \"\"\",\n        ),\n    )\n    assert package_name_from_path(tmp_path) == \"setup_cfg_project\"\n    assert package_name_from_setup_cfg(setup_cfg) == \"setup_cfg_project\"\n    missing = tmp_path / \"missing\" / \"setup.cfg\"\n    assert not missing.exists()\n    with pytest.raises(KeyError):\n        package_name_from_setup_cfg(missing)\n\n    setup_cfg2 = tmp_path / \"setup.cfg\"\n    setup_cfg2.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            [metadata]\n            yolo = missing\n            \"\"\",\n        ),\n    )\n    with pytest.raises(KeyError):\n        package_name_from_setup_cfg(setup_cfg2)\n\n\ndef test_package_name_from_setup_py_requires_literal_name(tmp_path: Path) -> None:\n    setup_py = tmp_path / \"setup.py\"\n    setup_py.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            from setuptools import setup\n            NAME = \"dynamic_name\"\n            setup(name=NAME)\n            \"\"\",\n        ),\n    )\n\n    with pytest.raises(\n        KeyError,\n        match=r\"Could not find the package name in the setup\\.py\",\n    ):\n        package_name_from_setup_py(setup_py)\n\n\ndef test_package_name_from_path_falls_back_on_invalid_pyproject(tmp_path: Path) -> None:\n    pyproject_toml = tmp_path / \"pyproject.toml\"\n    pyproject_toml.write_text(\"this is not valid toml = [\")\n\n    assert package_name_from_path(tmp_path) == tmp_path.name\n\n\ndef test_package_name_from_path_falls_back_on_invalid_setup_py(tmp_path: Path) -> None:\n    setup_py = tmp_path / \"setup.py\"\n    setup_py.write_text(\"from setuptools import setup\\nsetup(name='missing'\")\n\n    assert package_name_from_path(tmp_path) == tmp_path.name\n\n\ndef test_package_name_from_path_does_not_suppress_unexpected_errors(\n    tmp_path: Path,\n) -> None:\n    setup_py = tmp_path / \"setup.py\"\n    setup_py.write_text(\"from setuptools import setup\\nsetup(name='pkg')\")\n\n    with patch(\n        \"unidep.utils.package_name_from_setup_py\",\n        side_effect=RuntimeError(\"boom\"),\n    ), pytest.raises(RuntimeError, match=\"boom\"):\n        package_name_from_path(tmp_path)\n\n\ndef test_filter_python_dependencies_rejects_resolved_dict_input() -> None:\n    with pytest.raises(\n        TypeError,\n        match=\"now requires dependency entries\",\n    ):\n        filter_python_dependencies({})  # type: ignore[arg-type]\n"
  },
  {
    "path": "tests/test_unidep.py",
    "content": "\"\"\"unidep tests.\"\"\"\n\nfrom __future__ import annotations\n\nimport textwrap\nfrom pathlib import Path, PureWindowsPath\nfrom typing import TYPE_CHECKING, Any\n\nimport pytest\nfrom ruamel.yaml import YAML\n\nfrom unidep import (\n    create_conda_env_specification,\n    filter_python_dependencies,\n    find_requirements_files,\n    get_python_dependencies,\n    parse_local_dependencies,\n    parse_requirements,\n    write_conda_environment_file,\n)\nfrom unidep._conda_env import CondaEnvironmentSpec\nfrom unidep._conflicts import (\n    VersionConflictError,\n    _pop_unused_platforms_and_maybe_expand_none,\n    resolve_conflicts,\n)\nfrom unidep._setuptools_integration import _path_to_file_uri\nfrom unidep.platform_definitions import Platform, Spec\nfrom unidep.utils import is_pip_installable\n\nfrom .helpers import maybe_as_toml\n\nif TYPE_CHECKING:\n    import sys\n\n    from unidep.platform_definitions import CondaPip\n\n    if sys.version_info >= (3, 8):\n        from typing import Literal\n    else:  # pragma: no cover\n        from typing_extensions import Literal\n\n\nREPO_ROOT = Path(__file__).parent.parent\n\n\n@pytest.fixture(params=[\"toml\", \"yaml\"])\ndef setup_test_files(\n    request: pytest.FixtureRequest,\n    tmp_path: Path,\n) -> tuple[Path, Path]:\n    d1 = tmp_path / \"dir1\"\n    d1.mkdir()\n    f1 = d1 / \"requirements.yaml\"\n    f1.write_text(\"dependencies:\\n  - numpy\\n  - conda: mumps\")\n\n    d2 = tmp_path / \"dir2\"\n    d2.mkdir()\n    f2 = d2 / \"requirements.yaml\"\n    f2.write_text(\"dependencies:\\n  - pip: pandas\")\n    f1 = maybe_as_toml(request.param, f1)\n    f2 = maybe_as_toml(request.param, f2)\n    return (f1, f2)\n\n\ndef test_find_requirements_files(\n    tmp_path: Path,\n    setup_test_files: tuple[Path, Path],\n) -> None:\n    # Make sure to pass the depth argument correctly if your function expects it.\n    found_files = find_requirements_files(\n        tmp_path,\n        depth=1,\n        verbose=True,\n    )\n\n    # Convert found_files to absolute paths for comparison\n    absolute_results = sorted(str(p.resolve()) for p in found_files)\n    absolute_test_files = sorted(str(p.resolve()) for p in setup_test_files)\n\n    assert absolute_results == absolute_test_files\n\n\ndef test_find_requirements_files_depth(tmp_path: Path) -> None:\n    # Create a nested directory structure\n    (tmp_path / \"dir1\").mkdir()\n    (tmp_path / \"dir1/dir2\").mkdir()\n    (tmp_path / \"dir1/dir2/dir3\").mkdir()\n\n    # Create test files\n    (tmp_path / \"requirements.yaml\").touch()\n    (tmp_path / \"dir1/requirements.yaml\").touch()\n    (tmp_path / \"dir1/dir2/requirements.yaml\").touch()\n    (tmp_path / \"dir1/dir2/dir3/requirements.yaml\").touch()\n\n    # Test depth=0\n    assert len(find_requirements_files(tmp_path, depth=0)) == 1\n\n    # Test depth=1\n    assert len(find_requirements_files(tmp_path, depth=1)) == 2\n\n    # Test depth=2\n    assert len(find_requirements_files(tmp_path, depth=2)) == 3\n\n    # Test depth=3\n    assert len(find_requirements_files(tmp_path, depth=3)) == 4\n\n    # Test depth=4 (or more)\n    assert len(find_requirements_files(tmp_path, depth=4)) == 4\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_parse_requirements(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - foo >1 # [linux64]\n                - foo # [unix]\n                - bar >1\n                - bar\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    assert requirements.requirements == {\n        \"foo\": [\n            Spec(\n                name=\"foo\",\n                which=\"conda\",\n                selector=\"linux64\",\n                pin=\">1\",\n                identifier=\"c292b98a\",\n            ),\n            Spec(\n                name=\"foo\",\n                which=\"pip\",\n                selector=\"linux64\",\n                pin=\">1\",\n                identifier=\"c292b98a\",\n            ),\n            Spec(\n                name=\"foo\",\n                which=\"conda\",\n                selector=\"unix\",\n                identifier=\"530d9eaa\",\n            ),\n            Spec(\n                name=\"foo\",\n                which=\"pip\",\n                selector=\"unix\",\n                identifier=\"530d9eaa\",\n            ),\n        ],\n        \"bar\": [\n            Spec(\n                name=\"bar\",\n                which=\"conda\",\n                pin=\">1\",\n                identifier=\"08fd8713\",\n            ),\n            Spec(\n                name=\"bar\",\n                which=\"pip\",\n                pin=\">1\",\n                identifier=\"08fd8713\",\n            ),\n            Spec(\n                name=\"bar\",\n                which=\"conda\",\n                identifier=\"9e467fa1\",\n            ),\n            Spec(\n                name=\"bar\",\n                which=\"pip\",\n                identifier=\"9e467fa1\",\n            ),\n        ],\n    }\n\n\n@pytest.mark.parametrize(\"verbose\", [True, False])\ndef test_generate_conda_env_file(\n    tmp_path: Path,\n    verbose: bool,  # noqa: FBT001\n    setup_test_files: tuple[Path, Path],\n) -> None:\n    output_file = tmp_path / \"environment.yaml\"\n    requirements = parse_requirements(*setup_test_files, verbose=verbose)\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n    )\n\n    write_conda_environment_file(env_spec, str(output_file), verbose=verbose)\n\n    with output_file.open() as f, YAML(typ=\"safe\") as yaml:\n        env_data = yaml.load(f)\n        assert \"dependencies\" in env_data\n        assert \"numpy\" in env_data[\"dependencies\"]\n        assert {\"pip\": [\"pandas\"]} in env_data[\"dependencies\"]\n\n\ndef test_generate_conda_env_stdout(\n    setup_test_files: tuple[Path, Path],\n    capsys: pytest.CaptureFixture,\n) -> None:\n    requirements = parse_requirements(*setup_test_files)\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n    )\n    write_conda_environment_file(env_spec, output_file=None)\n    captured = capsys.readouterr()\n    assert \"dependencies\" in captured.out\n    assert \"numpy\" in captured.out\n    assert \"- pandas\" in captured.out\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_create_conda_env_specification_platforms(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - yolo  # [arm64]\n                - foo  # [linux64]\n                - conda: bar  # [win]\n                - pip: pip-package\n                - pip: pip-package2  # [arm64]\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p)\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n    )\n    assert env_spec.conda == [\n        {\"sel(linux)\": \"foo\"},\n        {\"sel(osx)\": \"yolo\"},\n        {\"sel(win)\": \"bar\"},\n    ]\n    expected_pip = [\n        \"pip-package\",\n        \"pip-package2; sys_platform == 'darwin' and platform_machine == 'arm64'\",\n    ]\n    assert env_spec.pip == expected_pip\n\n    # Test on two platforms\n    platforms: list[Platform] = [\"osx-arm64\", \"win-64\"]\n\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        platforms,\n    )\n    assert env_spec.conda == [{\"sel(osx)\": \"yolo\"}, {\"sel(win)\": \"bar\"}]\n    assert sorted(env_spec.pip) == sorted(expected_pip)\n\n    # Test with comment selector\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        platforms,\n        selector=\"comment\",\n    )\n    assert env_spec.conda == [\"yolo\", \"bar\"]\n    assert env_spec.pip == [\"pip-package\", \"pip-package2\"]\n    write_conda_environment_file(env_spec, str(tmp_path / \"environment.yaml\"))\n    with (tmp_path / \"environment.yaml\").open() as f:\n        text = \"\".join(f.readlines())\n        assert \"- yolo  # [arm64]\" in text\n        assert \"- bar # [win64]\" in text\n\n    with pytest.raises(ValueError, match=\"Invalid platform\"):\n        resolve_conflicts(\n            requirements.requirements,\n            [\"unknown-platform\"],  # type: ignore[list-item]\n        )\n\n\ndef test_verbose_output(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:\n    f = tmp_path / \"dir3\" / \"requirements.yaml\"\n    f.parent.mkdir()\n    f.write_text(\"dependencies:\\n  - scipy\")\n\n    find_requirements_files(tmp_path, verbose=True)\n    captured = capsys.readouterr()\n    assert \"Scanning in\" in captured.out\n    assert str(tmp_path / \"dir3\") in captured.out\n\n    parse_requirements(f, verbose=True)\n    captured = capsys.readouterr()\n    assert \"Parsing\" in captured.out\n    assert str(f) in captured.out\n\n    write_conda_environment_file(\n        CondaEnvironmentSpec(\n            channels=[],\n            pip_indices=[],\n            platforms=[],\n            conda=[],\n            pip=[],\n        ),\n        verbose=True,\n    )\n    captured = capsys.readouterr()\n    assert \"Generating environment file at\" in captured.out\n    assert \"Environment file generated successfully.\" in captured.out\n\n\ndef test_create_conda_env_specification_rejects_resolved_dict_input() -> None:\n    resolved: Any = {}\n    with pytest.raises(\n        TypeError,\n        match=\"now requires dependency entries\",\n    ):\n        create_conda_env_specification(resolved, [], [])\n\n\ndef test_pop_unused_platforms_removes_non_requested_platform() -> None:\n    linux_spec = Spec(name=\"foo\", which=\"conda\", identifier=\"linux\")\n    osx_spec = Spec(name=\"foo\", which=\"conda\", identifier=\"osx\")\n    platform_data: dict[Platform | None, dict[CondaPip, list[Spec]]] = {\n        \"linux-64\": {\"conda\": [linux_spec]},\n        \"osx-arm64\": {\"conda\": [osx_spec]},\n    }\n\n    _pop_unused_platforms_and_maybe_expand_none(platform_data, [\"osx-arm64\"])\n\n    assert platform_data == {\"osx-arm64\": {\"conda\": [osx_spec]}}\n\n\ndef test_extract_python_requires(setup_test_files: tuple[Path, Path]) -> None:\n    f1, f2 = setup_test_files\n    requires1 = get_python_dependencies(str(f1))\n    assert requires1.dependencies == [\"numpy\"]\n    requires2 = get_python_dependencies(str(f2))\n    assert requires2.dependencies == [\"pandas\"]\n\n    # Test with a file that doesn't exist\n    with pytest.raises(FileNotFoundError):\n        get_python_dependencies(\"nonexistent_file.yaml\", raises_if_missing=True)\n    assert (\n        get_python_dependencies(\n            \"nonexistent_file.yaml\",\n            raises_if_missing=False,\n        ).dependencies\n        == []\n    )\n\n\ndef test_pip_install_local_dependencies(tmp_path: Path) -> None:\n    p = tmp_path / \"pkg\" / \"requirements.yaml\"\n    p.parent.mkdir(exist_ok=True)\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - foo\n            local_dependencies:\n                - ../local_package\n            \"\"\",\n        ),\n    )\n    deps = get_python_dependencies(p, raises_if_missing=False)\n    assert deps.dependencies == [\"foo\"]\n\n    deps = get_python_dependencies(p, include_local_dependencies=True)\n    assert deps.dependencies == [\"foo\"]  # because the local package doesn't exist\n\n    local_package = tmp_path / \"local_package\"\n    local_package.mkdir(exist_ok=True, parents=True)\n    assert not is_pip_installable(local_package)\n    (local_package / \"setup.py\").touch()\n    assert is_pip_installable(local_package)\n    deps = get_python_dependencies(p, include_local_dependencies=True)\n    assert deps.dependencies == [\n        \"foo\",\n        f\"local_package @ {_path_to_file_uri(local_package)}\",\n    ]\n\n\ndef test_path_to_file_uri_handles_windows_drive() -> None:\n    uri = _path_to_file_uri(PureWindowsPath(\"D:/projects/Uni Dep\"))\n    assert uri == \"file:///D:/projects/Uni%20Dep\"\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_channels(toml_or_yaml: Literal[\"toml\", \"yaml\"], tmp_path: Path) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\"channels:\\n  - conda-forge\\n  - defaults\")\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    assert requirements.channels == [\"conda-forge\", \"defaults\"]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_surrounding_comments(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n            # This is a comment before\n                - yolo  # [osx]\n            # This is a comment after\n                # This is another comment\n                - foo  # [linux]\n                # And this is a comment after\n                - bar  # [win]\n                # Next is an empty comment\n                - baz  #\n                - pip: pip-package\n                #\n                - pip: pip-package2  # [osx]\n                #\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    assert requirements.requirements == {\n        \"yolo\": [\n            Spec(\n                name=\"yolo\",\n                which=\"conda\",\n                selector=\"osx\",\n                identifier=\"8b0c4c31\",\n            ),\n            Spec(\n                name=\"yolo\",\n                which=\"pip\",\n                selector=\"osx\",\n                identifier=\"8b0c4c31\",\n            ),\n        ],\n        \"foo\": [\n            Spec(\n                name=\"foo\",\n                which=\"conda\",\n                selector=\"linux\",\n                identifier=\"ecd4baa6\",\n            ),\n            Spec(\n                name=\"foo\",\n                which=\"pip\",\n                selector=\"linux\",\n                identifier=\"ecd4baa6\",\n            ),\n        ],\n        \"bar\": [\n            Spec(\n                name=\"bar\",\n                which=\"conda\",\n                selector=\"win\",\n                identifier=\"8528de75\",\n            ),\n            Spec(\n                name=\"bar\",\n                which=\"pip\",\n                selector=\"win\",\n                identifier=\"8528de75\",\n            ),\n        ],\n        \"baz\": [\n            Spec(\n                name=\"baz\",\n                which=\"conda\",\n                identifier=\"9e467fa1\",\n            ),\n            Spec(name=\"baz\", which=\"pip\", identifier=\"9e467fa1\"),\n        ],\n        \"pip-package\": [\n            Spec(\n                name=\"pip-package\",\n                which=\"pip\",\n                identifier=\"5813b64a\",\n            ),\n        ],\n        \"pip-package2\": [\n            Spec(\n                name=\"pip-package2\",\n                which=\"pip\",\n                selector=\"osx\",\n                identifier=\"1c0fa4c4\",\n            ),\n        ],\n    }\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_filter_pip_and_conda(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    # Setup a sample ParsedRequirements instance with platform selectors\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n              - conda: package1  # [linux64]\n              - conda: package2  # [osx64]\n              - pip: package3\n              - pip: package4  # [unix]\n              - common_package  # [unix]\n              - conda: shared_package  # [linux64]\n                pip: shared_package  # [win64]\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    assert requirements.requirements == {\n        \"package1\": [\n            Spec(\n                name=\"package1\",\n                which=\"conda\",\n                selector=\"linux64\",\n                identifier=\"c292b98a\",\n            ),\n        ],\n        \"package2\": [\n            Spec(\n                name=\"package2\",\n                which=\"conda\",\n                selector=\"osx64\",\n                identifier=\"b2ac468f\",\n            ),\n        ],\n        \"package3\": [\n            Spec(\n                name=\"package3\",\n                which=\"pip\",\n                identifier=\"08fd8713\",\n            ),\n        ],\n        \"package4\": [\n            Spec(\n                name=\"package4\",\n                which=\"pip\",\n                selector=\"unix\",\n                identifier=\"1d5d7757\",\n            ),\n        ],\n        \"common_package\": [\n            Spec(\n                name=\"common_package\",\n                which=\"conda\",\n                selector=\"unix\",\n                identifier=\"f78244dc\",\n            ),\n            Spec(\n                name=\"common_package\",\n                which=\"pip\",\n                selector=\"unix\",\n                identifier=\"f78244dc\",\n            ),\n        ],\n        \"shared_package\": [\n            Spec(\n                name=\"shared_package\",\n                which=\"conda\",\n                selector=\"linux64\",\n                identifier=\"1599d575\",\n            ),\n            Spec(\n                name=\"shared_package\",\n                which=\"pip\",\n                selector=\"win64\",\n                identifier=\"46630b59\",\n            ),\n        ],\n    }\n\n    resolved = resolve_conflicts(\n        requirements.requirements,\n        requirements.platforms,\n    )\n    assert resolved == {\n        \"package1\": {\n            \"linux-64\": {\n                \"conda\": Spec(\n                    name=\"package1\",\n                    which=\"conda\",\n                    selector=\"linux64\",\n                    identifier=\"c292b98a\",\n                ),\n            },\n        },\n        \"package2\": {\n            \"osx-64\": {\n                \"conda\": Spec(\n                    name=\"package2\",\n                    which=\"conda\",\n                    selector=\"osx64\",\n                    identifier=\"b2ac468f\",\n                ),\n            },\n        },\n        \"package3\": {\n            None: {\n                \"pip\": Spec(\n                    name=\"package3\",\n                    which=\"pip\",\n                    identifier=\"08fd8713\",\n                ),\n            },\n        },\n        \"package4\": {\n            \"linux-64\": {\n                \"pip\": Spec(\n                    name=\"package4\",\n                    which=\"pip\",\n                    selector=\"unix\",\n                    identifier=\"1d5d7757\",\n                ),\n            },\n            \"linux-aarch64\": {\n                \"pip\": Spec(\n                    name=\"package4\",\n                    which=\"pip\",\n                    selector=\"unix\",\n                    identifier=\"1d5d7757\",\n                ),\n            },\n            \"linux-ppc64le\": {\n                \"pip\": Spec(\n                    name=\"package4\",\n                    which=\"pip\",\n                    selector=\"unix\",\n                    identifier=\"1d5d7757\",\n                ),\n            },\n            \"osx-64\": {\n                \"pip\": Spec(\n                    name=\"package4\",\n                    which=\"pip\",\n                    selector=\"unix\",\n                    identifier=\"1d5d7757\",\n                ),\n            },\n            \"osx-arm64\": {\n                \"pip\": Spec(\n                    name=\"package4\",\n                    which=\"pip\",\n                    selector=\"unix\",\n                    identifier=\"1d5d7757\",\n                ),\n            },\n        },\n        \"common_package\": {\n            \"linux-64\": {\n                \"conda\": Spec(\n                    name=\"common_package\",\n                    which=\"conda\",\n                    selector=\"unix\",\n                    identifier=\"f78244dc\",\n                ),\n                \"pip\": Spec(\n                    name=\"common_package\",\n                    which=\"pip\",\n                    selector=\"unix\",\n                    identifier=\"f78244dc\",\n                ),\n            },\n            \"linux-aarch64\": {\n                \"conda\": Spec(\n                    name=\"common_package\",\n                    which=\"conda\",\n                    selector=\"unix\",\n                    identifier=\"f78244dc\",\n                ),\n                \"pip\": Spec(\n                    name=\"common_package\",\n                    which=\"pip\",\n                    selector=\"unix\",\n                    identifier=\"f78244dc\",\n                ),\n            },\n            \"linux-ppc64le\": {\n                \"conda\": Spec(\n                    name=\"common_package\",\n                    which=\"conda\",\n                    selector=\"unix\",\n                    identifier=\"f78244dc\",\n                ),\n                \"pip\": Spec(\n                    name=\"common_package\",\n                    which=\"pip\",\n                    selector=\"unix\",\n                    identifier=\"f78244dc\",\n                ),\n            },\n            \"osx-64\": {\n                \"conda\": Spec(\n                    name=\"common_package\",\n                    which=\"conda\",\n                    selector=\"unix\",\n                    identifier=\"f78244dc\",\n                ),\n                \"pip\": Spec(\n                    name=\"common_package\",\n                    which=\"pip\",\n                    selector=\"unix\",\n                    identifier=\"f78244dc\",\n                ),\n            },\n            \"osx-arm64\": {\n                \"conda\": Spec(\n                    name=\"common_package\",\n                    which=\"conda\",\n                    selector=\"unix\",\n                    identifier=\"f78244dc\",\n                ),\n                \"pip\": Spec(\n                    name=\"common_package\",\n                    which=\"pip\",\n                    selector=\"unix\",\n                    identifier=\"f78244dc\",\n                ),\n            },\n        },\n        \"shared_package\": {\n            \"linux-64\": {\n                \"conda\": Spec(\n                    name=\"shared_package\",\n                    which=\"conda\",\n                    selector=\"linux64\",\n                    identifier=\"1599d575\",\n                ),\n            },\n            \"win-64\": {\n                \"pip\": Spec(\n                    name=\"shared_package\",\n                    which=\"pip\",\n                    selector=\"win64\",\n                    identifier=\"46630b59\",\n                ),\n            },\n        },\n    }\n    # Pip\n    python_deps = filter_python_dependencies(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n    assert python_deps == [\n        \"common_package; sys_platform == 'linux' or sys_platform == 'darwin'\",\n        \"package3\",\n        \"package4; sys_platform == 'linux' or sys_platform == 'darwin'\",\n        \"shared_package; sys_platform == 'win32' and platform_machine == 'AMD64'\",\n    ]\n\n    # Conda\n    conda_env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        channels=requirements.channels,\n        pip_indices=requirements.pip_indices,\n        platforms=requirements.platforms,\n    )\n\n    def sort(x: list[dict[str, str]]) -> list[dict[str, str]]:\n        return sorted(x, key=lambda x: tuple(x.items()))\n\n    assert sort(conda_env_spec.conda) == sort(  # type: ignore[arg-type]\n        [\n            {\"sel(linux)\": \"package1\"},\n            {\"sel(osx)\": \"package2\"},\n            {\"sel(osx)\": \"common_package\"},\n            {\"sel(linux)\": \"common_package\"},\n            {\"sel(linux)\": \"shared_package\"},\n        ],\n    )\n    assert sorted(conda_env_spec.pip) == sorted(\n        [\n            \"package3\",\n            \"package4; sys_platform == 'linux' or sys_platform == 'darwin'\",\n            \"shared_package; sys_platform == 'win32' and platform_machine == 'AMD64'\",\n        ],\n    )\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_duplicates_with_version(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - foo >1 # [linux64]\n                - foo # [linux64]\n                - bar\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    assert requirements.requirements == {\n        \"foo\": [\n            Spec(\n                name=\"foo\",\n                which=\"conda\",\n                selector=\"linux64\",\n                pin=\">1\",\n                identifier=\"c292b98a\",\n            ),\n            Spec(\n                name=\"foo\",\n                which=\"pip\",\n                selector=\"linux64\",\n                pin=\">1\",\n                identifier=\"c292b98a\",\n            ),\n            Spec(\n                name=\"foo\",\n                which=\"conda\",\n                selector=\"linux64\",\n                identifier=\"dd6a8aaf\",\n            ),\n            Spec(\n                name=\"foo\",\n                which=\"pip\",\n                selector=\"linux64\",\n                identifier=\"dd6a8aaf\",\n            ),\n        ],\n        \"bar\": [\n            Spec(\n                name=\"bar\",\n                which=\"conda\",\n                identifier=\"08fd8713\",\n            ),\n            Spec(\n                name=\"bar\",\n                which=\"pip\",\n                identifier=\"08fd8713\",\n            ),\n        ],\n    }\n    resolved = resolve_conflicts(requirements.requirements, requirements.platforms)\n    assert resolved == {\n        \"foo\": {\n            \"linux-64\": {\n                \"conda\": Spec(\n                    name=\"foo\",\n                    which=\"conda\",\n                    selector=\"linux64\",\n                    pin=\">1\",\n                    identifier=\"c292b98a\",\n                ),\n                \"pip\": Spec(\n                    name=\"foo\",\n                    which=\"pip\",\n                    selector=\"linux64\",\n                    pin=\">1\",\n                    identifier=\"c292b98a\",\n                ),\n            },\n        },\n        \"bar\": {\n            None: {\n                \"conda\": Spec(\n                    name=\"bar\",\n                    which=\"conda\",\n                    identifier=\"08fd8713\",\n                ),\n                \"pip\": Spec(\n                    name=\"bar\",\n                    which=\"pip\",\n                    identifier=\"08fd8713\",\n                ),\n            },\n        },\n    }\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n    )\n    assert env_spec.conda == [\"bar\", {\"sel(linux)\": \"foo >1\"}]\n    assert env_spec.pip == []\n\n    python_deps = filter_python_dependencies(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n    assert python_deps == [\n        \"bar\",\n        \"foo >1; sys_platform == 'linux' and platform_machine == 'x86_64'\",\n    ]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_duplicates_different_platforms(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - foo >1 # [linux64]\n                - foo <=2 # [linux]\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    assert requirements.requirements == {\n        \"foo\": [\n            Spec(\n                name=\"foo\",\n                which=\"conda\",\n                selector=\"linux64\",\n                pin=\">1\",\n                identifier=\"c292b98a\",\n            ),\n            Spec(\n                name=\"foo\",\n                which=\"pip\",\n                selector=\"linux64\",\n                pin=\">1\",\n                identifier=\"c292b98a\",\n            ),\n            Spec(\n                name=\"foo\",\n                which=\"conda\",\n                selector=\"linux\",\n                pin=\"<=2\",\n                identifier=\"ecd4baa6\",\n            ),\n            Spec(\n                name=\"foo\",\n                which=\"pip\",\n                selector=\"linux\",\n                pin=\"<=2\",\n                identifier=\"ecd4baa6\",\n            ),\n        ],\n    }\n    resolved = resolve_conflicts(requirements.requirements, requirements.platforms)\n    assert resolved == {\n        \"foo\": {\n            \"linux-64\": {\n                \"conda\": Spec(\n                    name=\"foo\",\n                    which=\"conda\",\n                    pin=\">1,<=2\",\n                    identifier=\"c292b98a\",\n                ),\n                \"pip\": Spec(\n                    name=\"foo\",\n                    which=\"pip\",\n                    pin=\">1,<=2\",\n                    identifier=\"c292b98a\",\n                ),\n            },\n            \"linux-aarch64\": {\n                \"conda\": Spec(\n                    name=\"foo\",\n                    which=\"conda\",\n                    selector=\"linux\",\n                    pin=\"<=2\",\n                    identifier=\"ecd4baa6\",\n                ),\n                \"pip\": Spec(\n                    name=\"foo\",\n                    which=\"pip\",\n                    selector=\"linux\",\n                    pin=\"<=2\",\n                    identifier=\"ecd4baa6\",\n                ),\n            },\n            \"linux-ppc64le\": {\n                \"conda\": Spec(\n                    name=\"foo\",\n                    which=\"conda\",\n                    selector=\"linux\",\n                    pin=\"<=2\",\n                    identifier=\"ecd4baa6\",\n                ),\n                \"pip\": Spec(\n                    name=\"foo\",\n                    which=\"pip\",\n                    selector=\"linux\",\n                    pin=\"<=2\",\n                    identifier=\"ecd4baa6\",\n                ),\n            },\n        },\n    }\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n    )\n    assert env_spec.conda == [{\"sel(linux)\": \"foo <=2,>1\"}]\n    assert env_spec.pip == []\n\n    python_deps = filter_python_dependencies(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n    assert python_deps == [\n        \"foo <=2,>1; sys_platform == 'linux' and platform_machine == 'x86_64'\",\n        \"foo <=2; sys_platform == 'linux' and platform_machine == 'aarch64' or \"\n        \"sys_platform == 'linux' and platform_machine == 'ppc64le'\",\n    ]\n\n    # now only use linux-64\n    platforms: list[Platform] = [\"linux-64\"]\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        platforms,\n    )\n    assert env_spec.conda == [\"foo <=2,>1\"]\n    assert env_spec.pip == []\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_expand_none_with_different_platforms(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - foo >1 # [linux64]\n                - foo <3\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    assert requirements.requirements == {\n        \"foo\": [\n            Spec(\n                name=\"foo\",\n                which=\"conda\",\n                selector=\"linux64\",\n                pin=\">1\",\n                identifier=\"c292b98a\",\n            ),\n            Spec(\n                name=\"foo\",\n                which=\"pip\",\n                selector=\"linux64\",\n                pin=\">1\",\n                identifier=\"c292b98a\",\n            ),\n            Spec(\n                name=\"foo\",\n                which=\"conda\",\n                pin=\"<3\",\n                identifier=\"5eb93b8c\",\n            ),\n            Spec(\n                name=\"foo\",\n                which=\"pip\",\n                pin=\"<3\",\n                identifier=\"5eb93b8c\",\n            ),\n        ],\n    }\n    resolved = resolve_conflicts(requirements.requirements, requirements.platforms)\n    assert resolved == {\n        \"foo\": {\n            \"linux-64\": {\n                \"conda\": Spec(\n                    name=\"foo\",\n                    which=\"conda\",\n                    pin=\">1,<3\",\n                    identifier=\"c292b98a\",\n                ),\n                \"pip\": Spec(\n                    name=\"foo\",\n                    which=\"pip\",\n                    pin=\">1,<3\",\n                    identifier=\"c292b98a\",\n                ),\n            },\n            \"linux-aarch64\": {\n                \"conda\": Spec(\n                    name=\"foo\",\n                    which=\"conda\",\n                    pin=\"<3\",\n                    identifier=\"5eb93b8c\",\n                ),\n                \"pip\": Spec(\n                    name=\"foo\",\n                    which=\"pip\",\n                    pin=\"<3\",\n                    identifier=\"5eb93b8c\",\n                ),\n            },\n            \"linux-ppc64le\": {\n                \"conda\": Spec(\n                    name=\"foo\",\n                    which=\"conda\",\n                    pin=\"<3\",\n                    identifier=\"5eb93b8c\",\n                ),\n                \"pip\": Spec(\n                    name=\"foo\",\n                    which=\"pip\",\n                    pin=\"<3\",\n                    identifier=\"5eb93b8c\",\n                ),\n            },\n            \"osx-64\": {\n                \"conda\": Spec(\n                    name=\"foo\",\n                    which=\"conda\",\n                    pin=\"<3\",\n                    identifier=\"5eb93b8c\",\n                ),\n                \"pip\": Spec(\n                    name=\"foo\",\n                    which=\"pip\",\n                    pin=\"<3\",\n                    identifier=\"5eb93b8c\",\n                ),\n            },\n            \"osx-arm64\": {\n                \"conda\": Spec(\n                    name=\"foo\",\n                    which=\"conda\",\n                    pin=\"<3\",\n                    identifier=\"5eb93b8c\",\n                ),\n                \"pip\": Spec(\n                    name=\"foo\",\n                    which=\"pip\",\n                    pin=\"<3\",\n                    identifier=\"5eb93b8c\",\n                ),\n            },\n            \"win-64\": {\n                \"conda\": Spec(\n                    name=\"foo\",\n                    which=\"conda\",\n                    pin=\"<3\",\n                    identifier=\"5eb93b8c\",\n                ),\n                \"pip\": Spec(\n                    name=\"foo\",\n                    which=\"pip\",\n                    pin=\"<3\",\n                    identifier=\"5eb93b8c\",\n                ),\n            },\n        },\n    }\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n    )\n    assert env_spec.conda == [\n        {\"sel(linux)\": \"foo >1,<3\"},\n        {\"sel(osx)\": \"foo <3\"},\n        {\"sel(win)\": \"foo <3\"},\n    ]\n\n    assert env_spec.pip == []\n\n    python_deps = filter_python_dependencies(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n    assert python_deps == [\n        \"foo <3; sys_platform == 'linux' and platform_machine == 'aarch64' or \"\n        \"sys_platform == 'linux' and platform_machine == 'ppc64le' or \"\n        \"sys_platform == 'darwin' and platform_machine == 'x86_64' or \"\n        \"sys_platform == 'darwin' and platform_machine == 'arm64' or \"\n        \"sys_platform == 'win32' and platform_machine == 'AMD64'\",\n        \"foo >1,<3; sys_platform == 'linux' and platform_machine == 'x86_64'\",\n    ]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_different_pins_on_conda_and_pip(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - pip: foo >1\n                  conda: foo <1\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    assert requirements.requirements == {\n        \"foo\": [\n            Spec(\n                name=\"foo\",\n                which=\"conda\",\n                pin=\"<1\",\n                identifier=\"17e5d607\",\n            ),\n            Spec(\n                name=\"foo\",\n                which=\"pip\",\n                pin=\">1\",\n                identifier=\"17e5d607\",\n            ),\n        ],\n    }\n    resolved = resolve_conflicts(requirements.requirements, requirements.platforms)\n    assert resolved == {\n        \"foo\": {\n            None: {\n                \"conda\": Spec(\n                    name=\"foo\",\n                    which=\"conda\",\n                    pin=\"<1\",\n                    identifier=\"17e5d607\",\n                ),\n                \"pip\": Spec(\n                    name=\"foo\",\n                    which=\"pip\",\n                    pin=\">1\",\n                    identifier=\"17e5d607\",\n                ),\n            },\n        },\n    }\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n    )\n    assert env_spec.conda == [\"foo <1\"]\n\n    assert env_spec.pip == []\n\n    python_deps = filter_python_dependencies(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n    assert python_deps == [\"foo >1\"]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_pip_pinned_conda_not(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - pip: foo >1\n                  conda: foo\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    resolved = resolve_conflicts(requirements.requirements, requirements.platforms)\n    assert resolved == {\n        \"foo\": {\n            None: {\n                \"conda\": Spec(\n                    name=\"foo\",\n                    which=\"conda\",\n                    identifier=\"17e5d607\",\n                ),\n                \"pip\": Spec(\n                    name=\"foo\",\n                    which=\"pip\",\n                    pin=\">1\",\n                    identifier=\"17e5d607\",\n                ),\n            },\n        },\n    }\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n    )\n    assert env_spec.conda == []\n\n    assert env_spec.pip == [\"foo >1\"]\n\n    python_deps = filter_python_dependencies(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n    assert python_deps == [\"foo >1\"]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_conda_pinned_pip_not(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - pip: foo\n                  conda: foo >1\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    resolved = resolve_conflicts(requirements.requirements, requirements.platforms)\n    assert resolved == {\n        \"foo\": {\n            None: {\n                \"conda\": Spec(\n                    name=\"foo\",\n                    which=\"conda\",\n                    pin=\">1\",\n                    identifier=\"17e5d607\",\n                ),\n                \"pip\": Spec(\n                    name=\"foo\",\n                    which=\"pip\",\n                    identifier=\"17e5d607\",\n                ),\n            },\n        },\n    }\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n    )\n    assert env_spec.conda == [\"foo >1\"]\n\n    assert env_spec.pip == []\n\n    python_deps = filter_python_dependencies(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n    assert python_deps == [\"foo\"]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_get_python_dependencies_preserves_platform_specific_pip_with_pinned_conda(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            platforms:\n                - linux-64\n                - osx-arm64\n            dependencies:\n                - conda: mypackage >=1.0 variant* # [linux64]\n                  pip: mypackage # [linux64]\n                - mypackage # [arm64]\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n\n    deps = get_python_dependencies(p, verbose=False)\n\n    assert deps.dependencies == [\n        \"mypackage; sys_platform == 'linux' and platform_machine == 'x86_64' \"\n        \"or sys_platform == 'darwin' and platform_machine == 'arm64'\",\n    ]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_filter_python_dependencies_with_platforms(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - foo # [unix]\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    python_deps = filter_python_dependencies(\n        requirements.dependency_entries,\n        [\"linux-64\"],\n    )\n    assert python_deps == [\n        \"foo; sys_platform == 'linux' and platform_machine == 'x86_64'\",\n    ]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_conda_with_comments(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - adaptive # [linux64]\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n        selector=\"comment\",\n    )\n    assert env_spec.conda == [\"adaptive\"]\n    assert env_spec.pip == []\n    write_conda_environment_file(env_spec, str(tmp_path / \"environment.yaml\"))\n    with (tmp_path / \"environment.yaml\").open() as f:\n        lines = f.readlines()\n        dependency_line = next(line for line in lines if \"adaptive\" in line)\n        assert \"- adaptive  # [linux64]\" in dependency_line\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_duplicate_names(toml_or_yaml: Literal[\"toml\", \"yaml\"], tmp_path: Path) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - conda: flatbuffers\n                - pip: flatbuffers\n                  conda: python-flatbuffers\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n    )\n    assert env_spec.conda == [\"flatbuffers\", \"python-flatbuffers\"]\n    assert env_spec.pip == []\n\n    python_deps = filter_python_dependencies(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n    assert python_deps == [\"flatbuffers\"]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_conflicts_when_selector_comment(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - foo >1 # [linux64]\n                - foo <2 # [linux]\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n        selector=\"comment\",\n    )\n    assert env_spec.conda == [\"foo >1,<2\", \"foo <2\", \"foo <2\"]\n    assert env_spec.pip == []\n\n    write_conda_environment_file(env_spec, str(tmp_path / \"environment.yaml\"))\n\n    with (tmp_path / \"environment.yaml\").open() as f:\n        text = \"\".join(f.readlines())\n        assert \"- foo >1,<2  # [linux64]\" in text\n        assert \"- foo <2 # [aarch64]\" in text\n        assert \"- foo <2 # [ppc64le]\" in text\n\n    # With just [unix]\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - foo >1\n                - foo <2 # [unix]\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n        selector=\"comment\",\n    )\n    assert env_spec.conda == [\n        \"foo >1,<2\",\n        \"foo >1,<2\",\n        \"foo >1,<2\",\n        \"foo >1,<2\",\n        \"foo >1,<2\",\n        \"foo >1\",\n    ]\n    assert env_spec.pip == []\n\n    write_conda_environment_file(env_spec, str(tmp_path / \"environment.yaml\"))\n\n    with (tmp_path / \"environment.yaml\").open() as f:\n        text = \"\".join(f.readlines())\n        assert \"- foo >1,<2  # [linux64]\" in text\n        assert \"- foo >1,<2 # [osx64]\" in text\n        assert \"- foo >1,<2 # [arm64]\" in text\n        assert \"- foo >1,<2 # [aarch64]\" in text\n        assert \"- foo >1,<2 # [ppc64le]\" in text\n        assert \"- foo >1 # [win64]\" in text\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_platforms_section_in_yaml(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            platforms:\n                - linux-64\n                - osx-arm64\n            dependencies:\n                - foo\n                - bar # [win]\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n        selector=\"sel\",\n    )\n    assert env_spec.conda == [\"foo\"]\n    assert env_spec.pip == []\n    assert env_spec.platforms == [\"linux-64\", \"osx-arm64\"]\n    python_deps = filter_python_dependencies(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n    assert python_deps == [\"foo\"]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_platforms_section_in_yaml_similar_platforms(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            channels:\n                - conda-forge\n            platforms:\n                - linux-64\n                - linux-aarch64\n            dependencies:\n                - foo\n                - bar # [win]\n                - yolo <1 # [aarch64]\n                - yolo >1 # [linux64]\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    with pytest.raises(\n        ValueError,\n        match=\"Use selector='comment' instead\",\n    ):\n        create_conda_env_specification(\n            requirements.dependency_entries,\n            requirements.channels,\n            requirements.pip_indices,\n            requirements.platforms,\n            selector=\"sel\",\n        )\n    python_deps = filter_python_dependencies(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n    assert python_deps == [\n        \"foo\",\n        \"yolo <1; sys_platform == 'linux' and platform_machine == 'aarch64'\",\n        \"yolo >1; sys_platform == 'linux' and platform_machine == 'x86_64'\",\n    ]\n\n    # Test with comment selector\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n        selector=\"comment\",\n    )\n    assert env_spec.conda == [\"foo\", \"yolo >1\", \"yolo <1\"]\n    assert env_spec.pip == []\n\n    write_conda_environment_file(env_spec, str(tmp_path / \"environment.yaml\"))\n\n    with (tmp_path / \"environment.yaml\").open() as f:\n        text = \"\".join(f.readlines())\n        assert \"- yolo >1  # [linux64]\" in text\n        assert \"- yolo <1 # [aarch64]\" in text\n        assert \"platforms:\" in text\n        assert \"- linux-64\" in text\n        assert \"- linux-aarch64\" in text\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_conda_with_non_platform_comment(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            channels:\n                - conda-forge\n            dependencies:\n                - pip: qsimcirq  # [linux64]\n                - pip: slurm-usage  # added to avoid https://github.com/conda/conda-lock/pull/564\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n        selector=\"comment\",\n    )\n    assert env_spec.conda == []\n    assert env_spec.pip == [\"qsimcirq\", \"slurm-usage\"]\n    write_conda_environment_file(env_spec, str(tmp_path / \"environment.yaml\"))\n    with (tmp_path / \"environment.yaml\").open() as f:\n        lines = \"\".join(f.readlines())\n    assert \"- qsimcirq  # [linux64]\" in lines\n    assert \"- slurm-usage\" in lines\n    assert \"  - pip:\" in lines\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_pip_and_conda_different_name_on_linux64(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    # On linux64, the conda package is called \"cuquantum-python\" and\n    # the pip package is called \"cuquantum\". We test that not both\n    # packages are in the final environment file.\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            name: test\n            channels:\n              - conda-forge\n            dependencies:\n              - conda: cuquantum-python  # [linux64]\n                pip: cuquantum  # [linux64]\n            platforms:\n              - linux-64\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=True)\n    expected = {\n        \"cuquantum-python\": [\n            Spec(\n                name=\"cuquantum-python\",\n                which=\"conda\",\n                selector=\"linux64\",\n                identifier=\"c292b98a\",\n            ),\n        ],\n        \"cuquantum\": [\n            Spec(\n                name=\"cuquantum\",\n                which=\"pip\",\n                selector=\"linux64\",\n                identifier=\"c292b98a\",\n            ),\n        ],\n    }\n    assert requirements.requirements == expected\n    resolved = resolve_conflicts(requirements.requirements, requirements.platforms)\n    expected_resolved = {\n        \"cuquantum-python\": {\n            \"linux-64\": {\n                \"conda\": Spec(\n                    name=\"cuquantum-python\",\n                    which=\"conda\",\n                    selector=\"linux64\",\n                    identifier=\"c292b98a\",\n                ),\n            },\n        },\n        \"cuquantum\": {\n            \"linux-64\": {\n                \"pip\": Spec(\n                    name=\"cuquantum\",\n                    which=\"pip\",\n                    selector=\"linux64\",\n                    identifier=\"c292b98a\",\n                ),\n            },\n        },\n    }\n    assert resolved == expected_resolved\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n    )\n    assert env_spec.conda == [\"cuquantum-python\"]\n    assert env_spec.pip == []\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_parse_requirements_with_ignore_pin(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - foo >1\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, ignore_pins=[\"foo\"], verbose=False)\n    assert requirements.requirements == {\n        \"foo\": [\n            Spec(\n                name=\"foo\",\n                which=\"conda\",\n                identifier=\"17e5d607\",\n            ),\n            Spec(\n                name=\"foo\",\n                which=\"pip\",\n                identifier=\"17e5d607\",\n            ),\n        ],\n    }\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_parse_requirements_with_skip_dependency(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - foo >1\n                - bar\n                - baz\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(\n        p,\n        skip_dependencies=[\"foo\", \"bar\"],\n        verbose=False,\n    )\n    assert requirements.requirements == {\n        \"baz\": [\n            Spec(\n                name=\"baz\",\n                which=\"conda\",\n                identifier=\"08fd8713\",\n            ),\n            Spec(\n                name=\"baz\",\n                which=\"pip\",\n                identifier=\"08fd8713\",\n            ),\n        ],\n    }\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_pin_star_cuda(toml_or_yaml: Literal[\"toml\", \"yaml\"], tmp_path: Path) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - conda: qsimcirq * cuda*  # [linux64]\n                - conda: qsimcirq * cpu*  # [arm64]\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p)\n    assert requirements.requirements == {\n        \"qsimcirq\": [\n            Spec(\n                name=\"qsimcirq\",\n                which=\"conda\",\n                selector=\"linux64\",\n                pin=\"* cuda*\",\n                identifier=\"c292b98a\",\n            ),\n            Spec(\n                name=\"qsimcirq\",\n                which=\"conda\",\n                selector=\"arm64\",\n                pin=\"* cpu*\",\n                identifier=\"489f33e0\",\n            ),\n        ],\n    }\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_parse_requirements_with_overwrite_pins(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - foo >1\n                - conda: bar * cuda*\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(\n        p,\n        overwrite_pins=[\"foo=1\", \"bar * cpu*\"],\n        verbose=False,\n    )\n    assert requirements.requirements == {\n        \"foo\": [\n            Spec(\n                name=\"foo\",\n                which=\"conda\",\n                pin=\"=1\",\n                identifier=\"17e5d607\",\n            ),\n            Spec(\n                name=\"foo\",\n                which=\"pip\",\n                pin=\"=1\",\n                identifier=\"17e5d607\",\n            ),\n        ],\n        \"bar\": [\n            Spec(\n                name=\"bar\",\n                which=\"conda\",\n                pin=\"* cpu*\",\n                identifier=\"5eb93b8c\",\n            ),\n        ],\n    }\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_duplicate_names_different_platforms(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - pip: ray  # [arm64]\n                - conda: ray-core  # [linux64]\n                  pip: ray # [linux64]\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(\n        p,\n        overwrite_pins=[\"foo=1\", \"bar * cpu*\"],\n        verbose=False,\n    )\n    assert requirements.requirements == {\n        \"ray\": [\n            Spec(\n                name=\"ray\",\n                which=\"pip\",\n                selector=\"arm64\",\n                identifier=\"1b26c5b2\",\n            ),\n            Spec(\n                name=\"ray\",\n                which=\"pip\",\n                selector=\"linux64\",\n                identifier=\"dd6a8aaf\",\n            ),\n        ],\n        \"ray-core\": [\n            Spec(\n                name=\"ray-core\",\n                which=\"conda\",\n                selector=\"linux64\",\n                identifier=\"dd6a8aaf\",\n            ),\n        ],\n    }\n    platforms_arm64: list[Platform] = [\"osx-arm64\"]\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        platforms_arm64,\n    )\n    assert env_spec.conda == []\n    assert env_spec.pip == [\"ray\"]\n\n    platforms_linux64: list[Platform] = [\"linux-64\"]\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        platforms_linux64,\n    )\n    assert env_spec.conda == [\"ray-core\"]\n    assert env_spec.pip == []\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_with_unused_platform(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - adaptive # [linux64]\n                - rsync-time-machine >0.1 # [osx64]\n                - rsync-time-machine <3\n                - rsync-time-machine >1 # [linux64]\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    requirements = parse_requirements(p, verbose=False)\n    platforms: list[Platform] = [\"linux-64\"]\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        platforms,\n        selector=\"comment\",\n    )\n    assert env_spec.conda == [\"adaptive\", \"rsync-time-machine >1,<3\"]\n    assert env_spec.pip == []\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_sel_selector_merges_explicit_platform_pinnings(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            platforms:\n              - linux-64\n              - linux-aarch64\n              - linux-ppc64le\n            dependencies:\n              - foo >1 # [linux64]\n              - foo <=2 # [aarch64]\n              - foo <=2 # [ppc64le]\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n\n    requirements = parse_requirements(p, verbose=False)\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.platforms,\n        selector=\"sel\",\n    )\n    assert env_spec.conda == [{\"sel(linux)\": \"foo >1,<=2\"}]\n    assert env_spec.pip == []\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_pip_with_pinning(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p1 = tmp_path / \"p1\" / \"requirements.yaml\"\n    p1.parent.mkdir()\n    p1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - pip: qiskit-terra ==0.25.2.1\n                - pip: qiskit-terra ==0.25.2.2\n            \"\"\",\n        ),\n    )\n    p1 = maybe_as_toml(toml_or_yaml, p1)\n\n    requirements = parse_requirements(p1, verbose=False)\n    with pytest.raises(\n        VersionConflictError,\n        match=r\"Invalid version pinning '==0\\.25\\.2\\.1' for 'qiskit-terra'\",\n    ):\n        resolve_conflicts(requirements.requirements, requirements.platforms)\n    with pytest.raises(\n        VersionConflictError,\n        match=(\n            r\"Multiple exact version pinnings found: ==0\\.25\\.2\\.1, ==0\\.25\\.2\\.2 \"\n            r\"for `qiskit-terra`\"\n        ),\n    ):\n        create_conda_env_specification(\n            requirements.dependency_entries,\n            requirements.channels,\n            requirements.platforms,\n        )\n    with pytest.raises(\n        VersionConflictError,\n        match=(\n            r\"Multiple exact version pinnings found: ==0\\.25\\.2\\.1, ==0\\.25\\.2\\.2 \"\n            r\"for `qiskit-terra`\"\n        ),\n    ):\n        filter_python_dependencies(\n            requirements.dependency_entries,\n            requirements.platforms,\n        )\n\n    p2 = tmp_path / \"p2\" / \"requirements.yaml\"\n    p2.parent.mkdir()\n    p2.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - pip: qiskit-terra =0.25.2.1\n                - pip: qiskit-terra =0.25.2.1\n            \"\"\",\n        ),\n    )\n    p2 = maybe_as_toml(toml_or_yaml, p2)\n\n    requirements = parse_requirements(p2, verbose=False)\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        requirements.platforms,\n    )\n    assert env_spec.conda == []\n    assert env_spec.pip == [\"qiskit-terra ==0.25.2.1\"]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_pip_with_pinning_special_case_wildcard(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p1 = tmp_path / \"p1\" / \"requirements.yaml\"\n    p1.parent.mkdir()\n    p1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - pip: qsimcirq * cuda*\n                - pip: qsimcirq * cuda*\n            \"\"\",\n        ),\n    )\n    p1 = maybe_as_toml(toml_or_yaml, p1)\n    requirements = parse_requirements(p1, verbose=False)\n\n    resolved = resolve_conflicts(requirements.requirements, requirements.platforms)\n    assert resolved == {\n        \"qsimcirq\": {\n            None: {\n                \"pip\": Spec(\n                    name=\"qsimcirq\",\n                    which=\"pip\",\n                    pin=\"* cuda*\",\n                    identifier=\"17e5d607\",\n                ),\n            },\n        },\n    }\n\n    p2 = tmp_path / \"p2\" / \"requirements.yaml\"\n    p2.parent.mkdir()\n    p2.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - pip: qsimcirq * cuda*\n                - pip: qsimcirq * cpu*\n            \"\"\",\n        ),\n    )\n    p2 = maybe_as_toml(toml_or_yaml, p2)\n\n    requirements = parse_requirements(p2, verbose=False)\n\n    with pytest.raises(\n        VersionConflictError,\n        match=r\"Invalid version pinning '\\* cuda\\*'\",\n    ):\n        resolve_conflicts(requirements.requirements, requirements.platforms)\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_pip_with_pinning_special_case_git_repo(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p1 = tmp_path / \"p1\" / \"requirements.yaml\"\n    p1.parent.mkdir()\n    p1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - pip: adaptive @ git+https://github.com/python-adaptive/adaptive.git@main\n                - pip: adaptive @ git+https://github.com/python-adaptive/adaptive.git@main\n            \"\"\",\n        ),\n    )\n    p1 = maybe_as_toml(toml_or_yaml, p1)\n\n    requirements = parse_requirements(p1, verbose=False)\n\n    resolved = resolve_conflicts(requirements.requirements, requirements.platforms)\n    assert resolved == {\n        \"adaptive\": {\n            None: {\n                \"pip\": Spec(\n                    name=\"adaptive\",\n                    which=\"pip\",\n                    pin=\"@ git+https://github.com/python-adaptive/adaptive.git@main\",\n                    identifier=\"17e5d607\",\n                ),\n            },\n        },\n    }\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_not_equal(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p1 = tmp_path / \"p1\" / \"requirements.yaml\"\n    p1.parent.mkdir()\n    p1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - adaptive != 1.0.0\n                - adaptive <2\n            \"\"\",\n        ),\n    )\n    p1 = maybe_as_toml(toml_or_yaml, p1)\n\n    requirements = parse_requirements(p1, verbose=False)\n\n    resolved = resolve_conflicts(requirements.requirements, requirements.platforms)\n    assert resolved == {\n        \"adaptive\": {\n            None: {\n                \"conda\": Spec(\n                    name=\"adaptive\",\n                    which=\"conda\",\n                    pin=\"!=1.0.0,<2\",\n                    identifier=\"17e5d607\",\n                ),\n                \"pip\": Spec(\n                    name=\"adaptive\",\n                    which=\"pip\",\n                    pin=\"!=1.0.0,<2\",\n                    identifier=\"17e5d607\",\n                ),\n            },\n        },\n    }\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_dot_in_package_name(\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    tmp_path: Path,\n) -> None:\n    p1 = tmp_path / \"p1\" / \"requirements.yaml\"\n    p1.parent.mkdir()\n    p1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - ruamel.yaml\n            \"\"\",\n        ),\n    )\n    p1 = maybe_as_toml(toml_or_yaml, p1)\n\n    requirements = parse_requirements(p1, verbose=False)\n    assert requirements.requirements == {\n        \"ruamel.yaml\": [\n            Spec(name=\"ruamel.yaml\", which=\"conda\", identifier=\"17e5d607\"),\n            Spec(name=\"ruamel.yaml\", which=\"pip\", identifier=\"17e5d607\"),\n        ],\n    }\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_optional_dependencies(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p = tmp_path / \"p\" / \"requirements.yaml\"\n    p.parent.mkdir()\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - adaptive != 1.0.0\n                - adaptive <2\n            optional_dependencies:\n                test:\n                    - pytest\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n\n    requirements = parse_requirements(p, verbose=False, extras=\"*\")\n    assert requirements.optional_dependencies.keys() == {\"test\"}\n    assert requirements.optional_dependencies[\"test\"].keys() == {\"pytest\"}\n\n    requirements = parse_requirements(p, verbose=False, extras=[[\"test\"]])\n    with pytest.raises(ValueError, match=\"Cannot specify `extras` list\"):\n        parse_requirements(Path(f\"{p}[test]\"), verbose=False, extras=[[\"test\"]])\n    with pytest.raises(ValueError, match=\"Length of `extras`\"):\n        parse_requirements(p, verbose=False, extras=[[], []])\n    requirements2 = parse_requirements(Path(f\"{p}[test]\"), verbose=False)\n    assert requirements2.optional_dependencies == requirements.optional_dependencies\n    resolved = resolve_conflicts(\n        requirements.requirements,\n        requirements.platforms,\n        optional_dependencies=requirements.optional_dependencies,\n    )\n    assert resolved.keys() == {\"adaptive\", \"pytest\"}\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_optional_dependencies_multiple_sections(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p = tmp_path / \"p\" / \"requirements.yaml\"\n    p.parent.mkdir()\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            optional_dependencies:\n                test:\n                    - pytest\n                lint:\n                    - flake8\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n\n    requirements = parse_requirements(p, verbose=False, extras=[[\"test\"]])\n    assert requirements.optional_dependencies.keys() == {\"test\"}\n\n    requirements = parse_requirements(p, verbose=False, extras=[[\"lint\"]])\n    assert requirements.optional_dependencies.keys() == {\"lint\"}\n\n    requirements = parse_requirements(p, verbose=False, extras=[[\"test\", \"lint\"]])\n    assert requirements.optional_dependencies.keys() == {\"test\", \"lint\"}\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_optional_dependencies_get_python_dependencies(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p = tmp_path / \"p\" / \"requirements.yaml\"\n    p.parent.mkdir()\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            optional_dependencies:\n                test:\n                    - pytest\n                lint:\n                    - flake8\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n\n    deps = get_python_dependencies(f\"{p}[test]\", verbose=False)\n    assert deps.dependencies == []\n    assert deps.extras == {\"test\": [\"pytest\"], \"lint\": [\"flake8\"]}\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_pip_dep_with_extras(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p = tmp_path / \"p\" / \"requirements.yaml\"\n    p.parent.mkdir()\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - conda: adaptive\n                  pip: adaptive[notebook]\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n\n    requirements = parse_requirements(p, verbose=False, extras=\"*\")\n    assert requirements.optional_dependencies == {}\n    resolved = resolve_conflicts(\n        requirements.requirements,\n        requirements.platforms,\n        optional_dependencies=requirements.optional_dependencies,\n    )\n    assert resolved == {\n        \"adaptive\": {\n            None: {\n                \"conda\": Spec(\n                    name=\"adaptive\",\n                    which=\"conda\",\n                    pin=None,\n                    identifier=\"17e5d607\",\n                    selector=None,\n                ),\n            },\n        },\n        \"adaptive[notebook]\": {\n            None: {\n                \"pip\": Spec(\n                    name=\"adaptive[notebook]\",\n                    which=\"pip\",\n                    pin=None,\n                    identifier=\"17e5d607\",\n                    selector=None,\n                ),\n            },\n        },\n    }\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.platforms,\n    )\n    assert env_spec.conda == []\n    assert env_spec.pip == [\"adaptive[notebook]\"]\n\n    python_deps = filter_python_dependencies(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n    assert python_deps == [\"adaptive[notebook]\"]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_explicit_conda_pip_pair_with_different_names_prefers_pinned_pip(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - conda: python-graphviz\n                  pip: graphviz >1\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n\n    requirements = parse_requirements(p, verbose=False)\n\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.platforms,\n    )\n    assert env_spec.conda == []\n    assert env_spec.pip == [\"graphviz >1\"]\n\n    python_deps = filter_python_dependencies(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n    assert python_deps == [\"graphviz >1\"]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_same_source_final_collisions_merge_pip_extras(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - pip: foo[dev]\n                - conda: bar\n                  pip: foo[test]\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n\n    requirements = parse_requirements(p, verbose=False)\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.platforms,\n    )\n    assert env_spec.conda == []\n    assert env_spec.pip == [\"foo[dev,test]\"]\n\n    python_deps = filter_python_dependencies(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n    assert python_deps == [\"foo[dev,test]\"]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_cross_source_final_collisions_raise_for_conda_like_outputs(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - foo\n                - conda: bar\n                  pip: foo >1\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n\n    requirements = parse_requirements(p, verbose=False)\n    with pytest.raises(ValueError, match=\"Final Dependency Collision\"):\n        create_conda_env_specification(\n            requirements.dependency_entries,\n            requirements.channels,\n            requirements.platforms,\n        )\n\n    python_deps = filter_python_dependencies(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n    assert python_deps == [\"foo >1\"]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_same_name_cross_family_collisions_choose_deterministically(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - conda: foo\n                - pip: foo >1\n            platforms:\n                - linux-64\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n\n    requirements = parse_requirements(p, verbose=False)\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.platforms,\n    )\n    assert env_spec.conda == []\n    assert env_spec.pip == [\"foo >1\"]\n\n    python_deps = filter_python_dependencies(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n    assert python_deps == [\"foo >1\"]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_pip_pep440_constraints_fall_back_to_explicit_joined_string(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - pip: pkg ~=1.0\n                - pip: pkg <2\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n\n    requirements = parse_requirements(p, verbose=False)\n    env_spec = create_conda_env_specification(\n        requirements.dependency_entries,\n        requirements.channels,\n        requirements.platforms,\n    )\n    assert env_spec.conda == []\n    assert env_spec.pip == [\"pkg ~=1.0,<2\"]\n\n    python_deps = filter_python_dependencies(\n        requirements.dependency_entries,\n        requirements.platforms,\n    )\n    assert python_deps == [\"pkg ~=1.0,<2\"]\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\n@pytest.mark.parametrize(\n    (\"first_pin\", \"second_pin\"),\n    [\n        (\">1\", \"<1\"),\n        (\"~=1.0\", \"<1\"),\n        (\"==1\", \"!=1\"),\n    ],\n)\ndef test_pip_contradictory_pep440_constraints_raise(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    first_pin: str,\n    second_pin: str,\n) -> None:\n    p = tmp_path / \"requirements.yaml\"\n    p.write_text(\n        textwrap.dedent(\n            f\"\"\"\\\n            dependencies:\n                - pip: pkg {first_pin}\n                - pip: pkg {second_pin}\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n\n    requirements = parse_requirements(p, verbose=False)\n    with pytest.raises(VersionConflictError):\n        create_conda_env_specification(\n            requirements.dependency_entries,\n            requirements.channels,\n            requirements.platforms,\n        )\n    with pytest.raises(VersionConflictError):\n        filter_python_dependencies(\n            requirements.dependency_entries,\n            requirements.platforms,\n        )\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_local_dependency_in_dependencies_list(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p = tmp_path / \"p\" / \"requirements.yaml\"\n    p.parent.mkdir()\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - ../p  # self\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n    with pytest.raises(ValueError, match=r\"Use the `local_dependencies` section\"):\n        parse_requirements(p, verbose=False)\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_optional_dependencies_with_local_dependencies(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p1 = tmp_path / \"p1\" / \"requirements.yaml\"\n    p1.parent.mkdir()\n    p1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - adaptive\n            optional_dependencies:\n                test:\n                    - pytest\n            \"\"\",\n        ),\n    )\n    p1 = maybe_as_toml(toml_or_yaml, p1)\n\n    p2 = tmp_path / \"p2\" / \"requirements.yaml\"\n    p2.parent.mkdir()\n    p2.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numthreads\n            optional_dependencies:\n                local:\n                    - ../p1\n                    - black\n            \"\"\",\n        ),\n    )\n    p2 = maybe_as_toml(toml_or_yaml, p2)\n\n    requirements = parse_requirements(p2, verbose=True, extras=\"*\")\n    assert requirements.optional_dependencies.keys() == {\"local\"}\n    assert requirements.optional_dependencies[\"local\"].keys() == {\"black\"}\n    assert requirements.requirements.keys() == {\"adaptive\", \"numthreads\"}\n    resolved = resolve_conflicts(\n        requirements.requirements,\n        requirements.platforms,\n        optional_dependencies=requirements.optional_dependencies,\n    )\n    assert resolved.keys() == {\"adaptive\", \"numthreads\", \"black\"}\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_optional_dependencies_with_local_dependencies_with_extras(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n    capsys: pytest.CaptureFixture,\n) -> None:\n    p1 = tmp_path / \"p1\" / \"requirements.yaml\"\n    p1.parent.mkdir()\n    p1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - adaptive\n            optional_dependencies:\n                test:\n                    - pytest\n            \"\"\",\n        ),\n    )\n    p1 = maybe_as_toml(toml_or_yaml, p1)\n\n    p2 = tmp_path / \"p2\" / \"requirements.yaml\"\n    p2.parent.mkdir()\n    p2.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - numthreads\n            optional_dependencies:\n                local:\n                    - ../p1[test]\n            \"\"\",\n        ),\n    )\n    p2 = maybe_as_toml(toml_or_yaml, p2)\n    requirements = parse_requirements(p2, verbose=True, extras=\"*\")\n    # The deps in the 'test' section in p1 will be moved to the dependencies.\n    assert \"Removing empty\" in capsys.readouterr().out\n    assert not requirements.optional_dependencies.keys()\n    assert requirements.requirements.keys() == {\"numthreads\", \"adaptive\", \"pytest\"}\n\n    # The local dependency section should still exist in p2\n    loc = parse_local_dependencies(\n        Path(f\"{p2}[local]\"),\n        verbose=True,\n        check_pip_installable=False,\n    )\n    assert len(loc) == 1\n\n    resolved = resolve_conflicts(\n        requirements.requirements,\n        requirements.platforms,\n        optional_dependencies=requirements.optional_dependencies,\n    )\n    assert resolved.keys() == {\"adaptive\", \"numthreads\", \"pytest\"}\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_optional_dependencies_with_dicts(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p1 = tmp_path / \"p1\" / \"requirements.yaml\"\n    p1.parent.mkdir()\n    p1.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - adaptive\n            optional_dependencies:\n                flat:\n                    - conda: python-flatbuffers\n                      pip: flatbuffers\n            \"\"\",\n        ),\n    )\n    p1 = maybe_as_toml(toml_or_yaml, p1)\n\n    requirements = parse_requirements(p1, verbose=True, extras=\"*\")\n    assert requirements.optional_dependencies.keys() == {\"flat\"}\n    assert requirements.optional_dependencies[\"flat\"].keys() == {\n        \"python-flatbuffers\",\n        \"flatbuffers\",\n    }\n\n    resolved = resolve_conflicts(\n        requirements.requirements,\n        requirements.platforms,\n        optional_dependencies=requirements.optional_dependencies,\n    )\n    assert resolved.keys() == {\"adaptive\", \"python-flatbuffers\", \"flatbuffers\"}\n\n\n@pytest.mark.parametrize(\"toml_or_yaml\", [\"toml\", \"yaml\"])\ndef test_optional_dependencies_with_version_specifier(\n    tmp_path: Path,\n    toml_or_yaml: Literal[\"toml\", \"yaml\"],\n) -> None:\n    p = tmp_path / \"p\" / \"requirements.yaml\"\n    p.parent.mkdir()\n    p.write_text(\n        textwrap.dedent(\n            \"\"\"\\\n            dependencies:\n                - adaptive\n            optional_dependencies:\n                specific:\n                    - adaptive =0.13.2\n            \"\"\",\n        ),\n    )\n    p = maybe_as_toml(toml_or_yaml, p)\n\n    requirements = parse_requirements(p, verbose=False, extras=\"*\")\n    assert requirements.optional_dependencies.keys() == {\"specific\"}\n    assert requirements.optional_dependencies[\"specific\"].keys() == {\"adaptive\"}\n    assert (\n        requirements.optional_dependencies[\"specific\"][\"adaptive\"][0].pin == \"=0.13.2\"\n    )\n\n    requirements = parse_requirements(p, verbose=False, extras=[[\"specific\"]])\n    requirements2 = parse_requirements(Path(f\"{p}[specific]\"), verbose=False)\n    assert requirements2.optional_dependencies == requirements.optional_dependencies\n    resolved = resolve_conflicts(\n        requirements.requirements,\n        requirements.platforms,\n        optional_dependencies=requirements.optional_dependencies,\n    )\n    assert resolved.keys() == {\"adaptive\"}\n    assert resolved[\"adaptive\"][None][\"conda\"].pin == \"=0.13.2\"\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "\"\"\"Tests for the unidep.utils module.\"\"\"\n\nfrom __future__ import annotations\n\nimport importlib.metadata\nimport sys\nfrom pathlib import Path\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom unidep.platform_definitions import Platform, Selector, Spec\nfrom unidep.utils import (\n    PathWithExtras,\n    UnsupportedPlatformError,\n    build_pep508_environment_marker,\n    collect_selector_platforms,\n    escape_unicode,\n    extract_matching_platforms,\n    get_package_version,\n    identify_current_platform,\n    parse_package_str,\n    resolve_platforms,\n    split_path_and_extras,\n)\n\nif sys.version_info >= (3, 8):\n    from typing import get_args\nelse:  # pragma: no cover\n    from typing_extensions import get_args\n\n\ndef test_escape_unicode() -> None:\n    assert escape_unicode(\"foo\\\\n\") == \"foo\\n\"\n    assert escape_unicode(\"foo\\\\t\") == \"foo\\t\"\n\n\ndef test_build_pep508_environment_marker() -> None:\n    # Test with a single platform\n    assert (\n        build_pep508_environment_marker([\"linux-64\"])\n        == \"sys_platform == 'linux' and platform_machine == 'x86_64'\"\n    )\n\n    # Test with multiple platforms\n    assert (\n        build_pep508_environment_marker([\"linux-64\", \"osx-64\"])\n        == \"sys_platform == 'linux' and platform_machine == 'x86_64' or sys_platform == 'darwin' and platform_machine == 'x86_64'\"\n    )\n\n    # Test with an empty list\n    assert not build_pep508_environment_marker([])\n\n    # Test with a platform not in PEP508_MARKERS\n    assert not build_pep508_environment_marker([\"unknown-platform\"])  # type: ignore[list-item]\n\n    # Test with a mix of valid and invalid platforms\n    assert (\n        build_pep508_environment_marker([\"linux-64\", \"unknown-platform\"])  # type: ignore[list-item]\n        == \"sys_platform == 'linux' and platform_machine == 'x86_64'\"\n    )\n\n\ndef test_spec_rendering_helpers() -> None:\n    spec = Spec(\n        name=\"numpy\",\n        which=\"conda\",\n        pin=\"=1.26,>=1.20\",\n        identifier=\"abc\",\n        selector=\"linux64\",\n    )\n    assert spec.pprint() == \"numpy =1.26,>=1.20 # [linux64]\"\n    assert spec.name_with_pin() == \"numpy =1.26,>=1.20\"\n    assert spec.name_with_pin(is_pip=True) == \"numpy ==1.26,>=1.20\"\n\n\ndef test_detect_platform() -> None:\n    with patch(\"platform.system\", return_value=\"Linux\"), patch(\n        \"platform.machine\",\n        return_value=\"x86_64\",\n    ):\n        assert identify_current_platform() == \"linux-64\"\n\n    with patch(\"platform.system\", return_value=\"Linux\"), patch(\n        \"platform.machine\",\n        return_value=\"aarch64\",\n    ):\n        assert identify_current_platform() == \"linux-aarch64\"\n\n    with patch(\"platform.system\", return_value=\"Darwin\"), patch(\n        \"platform.machine\",\n        return_value=\"x86_64\",\n    ):\n        assert identify_current_platform() == \"osx-64\"\n\n    with patch(\"platform.system\", return_value=\"Darwin\"), patch(\n        \"platform.machine\",\n        return_value=\"arm64\",\n    ):\n        assert identify_current_platform() == \"osx-arm64\"\n\n    with patch(\"platform.system\", return_value=\"Windows\"), patch(\n        \"platform.machine\",\n        return_value=\"AMD64\",\n    ):\n        assert identify_current_platform() == \"win-64\"\n\n    with patch(\"platform.system\", return_value=\"Linux\"), patch(\n        \"platform.machine\",\n        return_value=\"unknown\",\n    ), pytest.raises(UnsupportedPlatformError, match=\"Unsupported Linux architecture\"):\n        identify_current_platform()\n\n    with patch(\"platform.system\", return_value=\"Darwin\"), patch(\n        \"platform.machine\",\n        return_value=\"unknown\",\n    ), pytest.raises(UnsupportedPlatformError, match=\"Unsupported macOS architecture\"):\n        identify_current_platform()\n\n    with patch(\"platform.system\", return_value=\"Windows\"), patch(\n        \"platform.machine\",\n        return_value=\"unknown\",\n    ), pytest.raises(\n        UnsupportedPlatformError,\n        match=\"Unsupported Windows architecture\",\n    ):\n        identify_current_platform()\n\n    with patch(\"platform.system\", return_value=\"Linux\"), patch(\n        \"platform.machine\",\n        return_value=\"ppc64le\",\n    ):\n        assert identify_current_platform() == \"linux-ppc64le\"\n\n    with patch(\"platform.system\", return_value=\"Unknown\"), patch(\n        \"platform.machine\",\n        return_value=\"x86_64\",\n    ), pytest.raises(UnsupportedPlatformError, match=\"Unsupported operating system\"):\n        identify_current_platform()\n\n\ndef test_collect_selector_platforms_with_optional_dependencies() -> None:\n    requirements = {\n        \"numpy\": [\n            Spec(name=\"numpy\", which=\"conda\", pin=\">=1.20\", identifier=\"a1\"),\n            Spec(\n                name=\"numpy\",\n                which=\"conda\",\n                pin=\">=1.20\",\n                identifier=\"a2\",\n                selector=\"linux64\",\n            ),\n        ],\n    }\n    optional_dependencies = {\n        \"dev\": {\n            \"pyobjc\": [\n                Spec(name=\"pyobjc\", which=\"pip\", identifier=\"b1\", selector=\"osx\"),\n            ],\n            \"pytest\": [Spec(name=\"pytest\", which=\"pip\", selector=\"win\")],\n        },\n    }\n\n    assert collect_selector_platforms(requirements, optional_dependencies) == [\n        \"linux-64\",\n        \"osx-64\",\n        \"osx-arm64\",\n        \"win-64\",\n    ]\n\n\n@pytest.mark.parametrize(\"empty_requested_platforms\", [None, []])\ndef test_resolve_platforms_precedence_and_fallback(\n    empty_requested_platforms: list[Platform] | None,\n) -> None:\n    assert resolve_platforms(\n        requested_platforms=[\"osx-64\", \"osx-64\"],\n        declared_platforms=[\"linux-64\"],\n        selector_platforms=[\"win-64\"],\n    ) == [\"osx-64\"]\n\n    assert resolve_platforms(\n        requested_platforms=empty_requested_platforms,\n        declared_platforms={\"linux-64\", \"linux-aarch64\"},\n        selector_platforms=[\"win-64\"],\n    ) == [\"linux-64\", \"linux-aarch64\"]\n\n    assert resolve_platforms(\n        requested_platforms=empty_requested_platforms,\n        declared_platforms=None,\n        selector_platforms=[\"win-64\", \"win-64\"],\n    ) == [\"win-64\"]\n\n    with patch(\"unidep.utils.identify_current_platform\", return_value=\"linux-64\"):\n        assert resolve_platforms(\n            requested_platforms=empty_requested_platforms,\n            declared_platforms=None,\n            selector_platforms=None,\n            default_current=True,\n        ) == [\"linux-64\"]\n\n    assert (\n        resolve_platforms(\n            requested_platforms=empty_requested_platforms,\n            declared_platforms=None,\n            selector_platforms=None,\n            default_current=False,\n        )\n        == []\n    )\n\n\ndef test_parse_package_str() -> None:\n    # Test with version pin\n    assert parse_package_str(\"numpy >=1.20.0\") == (\"numpy\", \">=1.20.0\", None)\n    assert parse_package_str(\"pandas<2.0,>=1.1.3\") == (\"pandas\", \"<2.0,>=1.1.3\", None)\n\n    # Test a name that includes a dash\n    assert parse_package_str(\"python-yolo>=1.20.0\") == (\"python-yolo\", \">=1.20.0\", None)\n\n    # Test with multiple version conditions\n    assert parse_package_str(\"scipy>=1.2.3, <1.3\") == (\"scipy\", \">=1.2.3, <1.3\", None)\n\n    # Test with no version pin\n    assert parse_package_str(\"matplotlib\") == (\"matplotlib\", None, None)\n\n    # Test with whitespace variations\n    assert parse_package_str(\"requests >= 2.25\") == (\"requests\", \">= 2.25\", None)\n\n    # Test when installing from a URL\n    url = \"https://github.com/python-adaptive/adaptive.git@main\"\n    pin = f\"@ git+{url}\"\n    assert parse_package_str(f\"adaptive {pin}\") == (\"adaptive\", pin, None)\n\n    # Test with invalid input\n    with pytest.raises(ValueError, match=\"Invalid package string\"):\n        parse_package_str(\">=1.20.0 numpy\")\n\n\ndef test_path_with_extras_eq_handles_non_matching_object() -> None:\n    path_with_extras = PathWithExtras(Path(\"requirements.yaml\"), [\"dev\"])\n    assert path_with_extras.__eq__(object()) is NotImplemented\n\n\ndef test_get_package_version_missing_package(monkeypatch: pytest.MonkeyPatch) -> None:\n    monkeypatch.setattr(\n        importlib.metadata,\n        \"version\",\n        lambda _name: (_ for _ in ()).throw(importlib.metadata.PackageNotFoundError),\n    )\n    assert get_package_version(\"definitely-not-installed\") is None\n\n\ndef test_parse_package_str_with_selector() -> None:\n    # Test with version pin\n    assert parse_package_str(\"numpy >=1.20.0:linux64\") == (\n        \"numpy\",\n        \">=1.20.0\",\n        \"linux64\",\n    )\n    assert parse_package_str(\"pandas<2.0,>=1.1.3:osx\") == (\n        \"pandas\",\n        \"<2.0,>=1.1.3\",\n        \"osx\",\n    )\n\n    # Test with multiple version conditions\n    assert parse_package_str(\"scipy>=1.2.3, <1.3:win\") == (\n        \"scipy\",\n        \">=1.2.3, <1.3\",\n        \"win\",\n    )\n\n    # Test with no version pin\n    assert parse_package_str(\"matplotlib:win\") == (\"matplotlib\", None, \"win\")\n\n    # Test with whitespace variations\n    assert parse_package_str(\"requests >= 2.25:win\") == (\"requests\", \">= 2.25\", \"win\")\n\n    # Test when installing from a URL\n    url = \"https://github.com/python-adaptive/adaptive.git@main\"\n    pin = f\"@ git+{url}\"\n    assert parse_package_str(f\"adaptive {pin}:win\") == (\"adaptive\", pin, \"win\")\n\n    for sel in get_args(Selector):\n        assert parse_package_str(f\"numpy:{sel}\") == (\"numpy\", None, sel)\n\n    # Test with multiple selectors\n    assert parse_package_str(\"numpy:linux64 win64\") == (\"numpy\", None, \"linux64 win64\")\n    with pytest.raises(ValueError, match=\"Invalid platform selector: `unknown`\"):\n        assert parse_package_str(\"numpy:linux64 unknown\")\n\n\ndef test_parse_package_str_with_extras() -> None:\n    assert parse_package_str(\"numpy[full]\") == (\"numpy[full]\", None, None)\n    assert parse_package_str(\"numpy[full]:win\") == (\"numpy[full]\", None, \"win\")\n    assert parse_package_str(\"numpy[full]>1.20.0:win\") == (\n        \"numpy[full]\",\n        \">1.20.0\",\n        \"win\",\n    )\n\n    assert parse_package_str(\"../path/to/package[full]\") == (\n        \"../path/to/package[full]\",\n        None,\n        None,\n    )\n    assert parse_package_str(\"../path/to/package[full]:win\") == (\n        \"../path/to/package[full]\",\n        None,\n        \"win\",\n    )\n    assert parse_package_str(\"../path/to/package[full]>1.20.0:win\") == (\n        \"../path/to/package[full]\",\n        \">1.20.0\",\n        \"win\",\n    )\n\n    assert parse_package_str(\"python-yolo[full]>1.20.0:win\") == (\n        \"python-yolo[full]\",\n        \">1.20.0\",\n        \"win\",\n    )\n\n\ndef test_extract_matching_platforms() -> None:\n    # Test with a line having a linux selector\n    content_linux = \"dependency1  # [linux]\"\n    assert set(extract_matching_platforms(content_linux)) == {\n        \"linux-64\",\n        \"linux-aarch64\",\n        \"linux-ppc64le\",\n    }\n\n    # Test with a line having a win selector\n    content_win = \"dependency2  # [win]\"\n    assert set(extract_matching_platforms(content_win)) == {\"win-64\"}\n\n    # Test with a line having an osx64 selector\n    content_osx64 = \"dependency3  # [osx64]\"\n    assert set(extract_matching_platforms(content_osx64)) == {\"osx-64\"}\n\n    # Test with a line having no selector\n    content_none = \"dependency4\"\n    assert extract_matching_platforms(content_none) == []\n\n    # Test with a comment line\n    content_comment = \"# This is a comment\"\n    assert extract_matching_platforms(content_comment) == []\n\n    # Test with a line having a unix selector\n    content_unix = \"dependency5  # [unix]\"\n    expected_unix = {\n        \"linux-64\",\n        \"linux-aarch64\",\n        \"linux-ppc64le\",\n        \"osx-64\",\n        \"osx-arm64\",\n    }\n    assert set(extract_matching_platforms(content_unix)) == expected_unix\n\n    # Test with a line having multiple selectors\n    content_multi = \"dependency7  # [linux64 unix]\"\n    expected_multi = {\n        \"linux-64\",\n        \"linux-aarch64\",\n        \"linux-ppc64le\",\n        \"osx-64\",\n        \"osx-arm64\",\n    }\n    assert set(extract_matching_platforms(content_multi)) == expected_multi\n\n    # Test with a line having multiple []\n    content_multi = \"dependency7  # [linux64] [win]\"\n    with pytest.raises(ValueError, match=\"Multiple bracketed selectors\"):\n        extract_matching_platforms(content_multi)\n\n    incorrect_platform = \"dependency8  # [unknown-platform]\"\n    with pytest.raises(ValueError, match=\"Invalid platform selector\"):\n        extract_matching_platforms(incorrect_platform)\n\n\ndef test_split_path_and_extras() -> None:\n    # parse_with_extras\n    s = \"any/path[something, another]\"\n    path, extras = split_path_and_extras(s)\n    assert path == Path(\"any/path\")\n    assert extras == [\"something\", \"another\"]\n    pe = PathWithExtras(path, extras)\n    assert pe.path_with_extras == Path(\"any/path[something,another]\")\n\n    # parse_without_extras\n    s = \"any/path\"\n    path, extras = split_path_and_extras(s)\n    assert path == Path(\"any/path\")\n    assert extras == []\n    assert PathWithExtras(path, extras).path_with_extras == Path(s)\n\n    # parse_incorrect_format\n    # Technically this path is not correct, but we don't check for multiple []\n    s = \"any/path[something][another]\"\n    path, extras = split_path_and_extras(s)\n    assert path == Path(\"any/path[something]\")\n    assert extras == [\"another\"]\n    assert PathWithExtras(path, extras).path_with_extras == Path(s)\n\n    # parse_empty_string\n    s = \"\"\n    path, extras = split_path_and_extras(s)\n    assert path == Path()\n    assert extras == []\n    assert PathWithExtras(path, extras).path_with_extras == Path(s)\n\n    s = \"any/path[something]/other\"\n    path, extras = split_path_and_extras(s)\n    assert path == Path(\"any/path[something]/other\")\n    assert extras == []\n    assert PathWithExtras(path, extras).path_with_extras == Path(s)\n\n    s = \"any/path[something]/other[foo]\"\n    path, extras = split_path_and_extras(s)\n    assert path == Path(\"any/path[something]/other\")\n    assert extras == [\"foo\"]\n    assert PathWithExtras(path, extras).path_with_extras == Path(s)\n\n    s = \"any/path]something[\"\n    path, extras = split_path_and_extras(s)\n    assert path == Path(\"any/path]something[\")\n    assert extras == []\n    assert PathWithExtras(path, extras).path_with_extras == Path(s)\n\n    s = \"any/path[something\"\n    path, extras = split_path_and_extras(s)\n    assert path == Path(\"any/path[something\")\n    assert extras == []\n    assert PathWithExtras(path, extras).path_with_extras == Path(s)\n\n    s = \"any/path]something]\"\n    path, extras = split_path_and_extras(s)\n    assert path == Path(\"any/path]something]\")\n    assert extras == []\n    assert PathWithExtras(path, extras).path_with_extras == Path(s)\n"
  },
  {
    "path": "tests/test_version_conflicts.py",
    "content": "\"\"\"Tests for the version conflict resolution logic.\"\"\"\n\nfrom __future__ import annotations\n\nimport pytest\n\nfrom unidep._conflicts import (\n    ALL_VERSION_OPERATORS,\n    VersionConflictError,\n    _combine_pinning_within_platform,\n    _is_redundant,\n    _is_valid_pinning,\n    _parse_pinning,\n    combine_version_pinnings,\n    extract_version_operator,\n)\nfrom unidep.platform_definitions import Spec\n\n\ndef test_combining_versions() -> None:\n    data = {\n        None: {\n            \"conda\": [\n                Spec(name=\"numpy\", which=\"conda\", pin=\">1\"),\n                Spec(name=\"numpy\", which=\"conda\", pin=\"<2\"),\n            ],\n        },\n    }\n    resolved = _combine_pinning_within_platform(data)  # type: ignore[arg-type]\n    assert resolved == {\n        None: {\n            \"conda\": Spec(name=\"numpy\", which=\"conda\", pin=\">1,<2\"),\n        },\n    }\n\n\n@pytest.mark.parametrize(\"operator\", [\"<\", \"<=\", \">\", \">=\", \"=\"])\n@pytest.mark.parametrize(\"version\", [\"1\", \"1.0\", \"1.0.0\", \"1.0.0rc1\"])\ndef test_is_valid_pinning(operator: str, version: str) -> None:\n    assert _is_valid_pinning(f\"{operator}{version}\")\n\n\n@pytest.mark.parametrize(\n    (\"pinnings\", \"expected\"),\n    [\n        ([\" > 0.0.1\", \" < 2\", \" = 1.0.0\"], \"=1.0.0\"),\n        ([\"<2\", \">1\"], \"<2,>1\"),\n        ([\">1\", \"<2\"], \">1,<2\"),\n        ([\"<3\", \"<=3\", \"<4\"], \"<3\"),\n        ([\"=1\", \"=1\"], \"=1\"),\n        ([\"=2\", \"<3\", \"<=3\", \"<4\"], \"=2\"),\n        ([\"=2\", \">1\", \"<3\"], \"=2\"),\n        ([\"=3\", \">=2\", \"<=4\"], \"=3\"),\n        ([\"=3\", \">1\", \"<4\"], \"=3\"),\n        ([\"=3\", \">2\", \"<4\"], \"=3\"),\n        ([\">=1\", \"<=1\"], \">=1,<=1\"),\n        ([\">=1\", \">=1\", \"=1\"], \"=1\"),\n        ([\">=1\", \">0\", \"<=3\", \"<4\"], \">=1,<=3\"),\n        ([\">=1\", \">0\", \"<=3\", \"<4\", \"!=1.5\"], \">=1,<=3,!=1.5\"),\n        ([\">=2\", \"<=2\"], \">=2,<=2\"),\n        ([\">=2\", \"<3\"], \">=2,<3\"),\n        ([\">0.0.1\", \"<2\", \"=1.0.0\"], \"=1.0.0\"),\n        ([\">1\", \"<=3\", \"<4\"], \">1,<=3\"),\n        ([\">1\", \"<=3\"], \">1,<=3\"),\n        # TODO #67: !=5 should be removed but this is not yet implemented  # noqa: TD004, FIX002\n        # However, this is not a problem here because !=5 is redundant\n        # as it is outside the range of >1 and <=3\n        ([\">1\", \"<=3\", \"!=5\"], \">1,<=3,!=5\"),\n        ([\">1\", \">=1\", \"<3\", \"<=3\", \"\"], \">1,<3\"),\n        ([\">1\"], \">1\"),\n        ([], \"\"),\n    ],\n)\ndef test_combine_version_pinnings(pinnings: list[str], expected: str) -> None:\n    assert combine_version_pinnings(pinnings) == expected\n    # Try reversing the order of the pinnings\n    if \",\" not in expected:\n        assert combine_version_pinnings(pinnings[::-1]) == expected\n    else:\n        parts = expected.split(\",\")\n        assert combine_version_pinnings(pinnings[::-1]) == \",\".join(parts[::-1])\n\n\n@pytest.mark.parametrize(\n    \"pinnings\",\n    [\n        [\"abc\", \"def\"],\n        [\"==abc\", \">2\"],\n        [\"<=>abc\", \">2\"],\n        [\">1\", \"abc\", \"<=3\", \"\"],\n        [\"abc\", \">=1\", \"<=2\"],\n        [\"3\", \"6\"],\n        [\">\", \"<\"],\n    ],\n)\ndef test_invalid_pinnings(pinnings: list[str]) -> None:\n    with pytest.raises(VersionConflictError, match=\"Invalid version pinning\"):\n        assert combine_version_pinnings(pinnings)\n\n\n@pytest.mark.parametrize(\n    \"pinnings\",\n    [[\">2\", \"<1\"], [\"<1\", \">2\"], [\">1\", \"<1\"], [\"<=1\", \">1\"], [\">1\", \"<=1\"]],\n)\ndef test_contradictory_pinnings(pinnings: list[str]) -> None:\n    p1, p2 = pinnings\n    with pytest.raises(\n        VersionConflictError,\n        match=f\"Contradictory version pinnings found for `None`: {p1} and {p2}\",\n    ):\n        combine_version_pinnings(pinnings)\n\n\ndef test_exact_pinning_with_contradictory_ranges() -> None:\n    with pytest.raises(\n        VersionConflictError,\n        match=\"Contradictory version pinnings found for `None`: =3 and <2\",\n    ):\n        combine_version_pinnings([\"=3\", \"<2\", \">4\"])\n\n    with pytest.raises(\n        VersionConflictError,\n        match=\"Contradictory version pinnings found for `None`: =3 and <1\",\n    ):\n        assert combine_version_pinnings([\"=3\", \"<1\", \">4\"])\n\n\ndef test_multiple_exact_pinnings() -> None:\n    with pytest.raises(\n        VersionConflictError,\n        match=\"Multiple exact version pinnings found: =2, =3\",\n    ):\n        combine_version_pinnings([\"=2\", \"=3\"])\n\n\ndef test_general_contradictory_pinnings() -> None:\n    # This test ensures that contradictory non-exact pinnings raise a VersionConflictError\n    with pytest.raises(\n        VersionConflictError,\n        match=\"Contradictory version pinnings found for `None`: >=2 and <1\",\n    ):\n        combine_version_pinnings([\">=2\", \"<1\"])\n\n\ndef test_is_redundant() -> None:\n    assert _is_redundant(\">2\", [\">5\"])\n    assert not _is_redundant(\">5\", [\">2\"])\n    assert _is_redundant(\"<5\", [\"<2\"])\n    assert _is_redundant(\">=2\", [\">2\"])\n    assert not _is_redundant(\">2\", [\">=2\"])\n\n\n@pytest.mark.parametrize(\"pinning\", [\"<<1\", \">>1\", \"=<1\", \"=>1\"])\ndef test_invalid_parse_pinning(pinning: str) -> None:\n    with pytest.raises(\n        VersionConflictError,\n        match=f\"Invalid version pinning: '{pinning}'\",\n    ):\n        _parse_pinning(pinning)\n\n\n@pytest.mark.parametrize(\"op\", ALL_VERSION_OPERATORS)\ndef test_extract_version_operator_all_operators(op: str) -> None:\n    assert extract_version_operator(f\"{op}1.0\") == op\n\n\n@pytest.mark.parametrize(\n    \"constraint\",\n    [\"1.0\", \"abc\", \"\", \"hello world\"],\n)\ndef test_extract_version_operator_no_operator(constraint: str) -> None:\n    assert extract_version_operator(constraint) == \"\"\n\n\ndef test_extract_version_operator_strips_whitespace() -> None:\n    assert extract_version_operator(\"  >=1.0  \") == \">=\"\n    assert extract_version_operator(\"  <2.0\") == \"<\"\n    assert extract_version_operator(\"  1.0  \") == \"\"\n"
  },
  {
    "path": "unidep/__init__.py",
    "content": "\"\"\"unidep - Unified Conda and Pip requirements management.\"\"\"\n\nfrom unidep._conda_env import (\n    create_conda_env_specification,\n    write_conda_environment_file,\n)\nfrom unidep._dependencies_parsing import (\n    find_requirements_files,\n    parse_local_dependencies,\n    parse_requirements,\n)\nfrom unidep._setuptools_integration import (\n    filter_python_dependencies,\n    get_python_dependencies,\n)\nfrom unidep._version import __version__\n\n__all__ = [\n    \"__version__\",\n    \"create_conda_env_specification\",\n    \"filter_python_dependencies\",\n    \"find_requirements_files\",\n    \"get_python_dependencies\",\n    \"parse_local_dependencies\",\n    \"parse_requirements\",\n    \"write_conda_environment_file\",\n]\n"
  },
  {
    "path": "unidep/_cli.py",
    "content": "#!/usr/bin/env python3\n\"\"\"unidep - Unified Conda and Pip requirements management.\n\nThis module provides a command-line tool for managing conda environment.yaml files.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport functools\nimport importlib.util\nimport itertools\nimport json\nimport os\nimport platform\nimport shutil\nimport subprocess\nimport sys\nimport time\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nfrom ruamel.yaml import YAML\n\nfrom unidep._conda_env import (\n    create_conda_env_specification,\n    write_conda_environment_file,\n)\nfrom unidep._conda_lock import conda_lock_command\nfrom unidep._dependencies_parsing import (\n    DependencyEntry,\n    _load,\n    find_requirements_files,\n    parse_local_dependencies,\n    parse_requirements,\n)\nfrom unidep._pixi import generate_pixi_toml\nfrom unidep._setuptools_integration import (\n    filter_python_dependencies,\n    get_python_dependencies,\n)\nfrom unidep._version import __version__\nfrom unidep.platform_definitions import Platform\nfrom unidep.utils import (\n    add_comment_to_file,\n    escape_unicode,\n    get_package_version,\n    identify_current_platform,\n    is_pip_installable,\n    parse_folder_or_filename,\n    parse_package_str,\n    resolve_platforms,\n    warn,\n)\n\nif sys.version_info >= (3, 8):\n    from typing import Literal, get_args\nelse:  # pragma: no cover\n    from typing_extensions import Literal, get_args\n\nif TYPE_CHECKING:\n    from collections.abc import Sequence\n\ntry:  # pragma: no cover\n    from rich_argparse import RichHelpFormatter\n\n    class _HelpFormatter(RichHelpFormatter):\n        def _get_help_string(self, action: argparse.Action) -> str | None:\n            # escapes \"[\" in text, otherwise e.g., [linux] is removed\n            if action.help is not None:\n                return action.help.replace(\"[\", r\"\\[\")\n            return None\nexcept ImportError:  # pragma: no cover\n    from argparse import HelpFormatter as _HelpFormatter  # type: ignore[assignment]\n\n_DEP_FILES = \"`requirements.yaml` or `pyproject.toml`\"\nCondaExecutable = Literal[\"conda\", \"mamba\", \"micromamba\"]\n\n\ndef _flatten_selected_dependency_entries(\n    dependency_entries: list[DependencyEntry],\n    optional_dependency_entries: dict[str, list[DependencyEntry]],\n) -> list[DependencyEntry]:\n    entries = list(dependency_entries)\n    for group_entries in optional_dependency_entries.values():\n        entries.extend(group_entries)\n    return entries\n\n\ndef _collect_available_optional_dependency_groups(\n    found_files: list[Path],\n) -> list[str]:\n    # Inspect only the top-level files so local-only groups remain visible\n    # without traversing local dependencies.\n    yaml = YAML(typ=\"rt\")\n    groups: set[str] = set()\n    for found_file in found_files:\n        groups.update(_load(found_file, yaml).get(\"optional_dependencies\", {}))\n    return sorted(groups)\n\n\ndef _merge_optional_dependency_extras(\n    *,\n    found_files: list[Path],\n    optional_dependencies: list[str],\n    all_optional_dependencies: bool,\n) -> list[list[str]] | Literal[\"*\"] | None:\n    if all_optional_dependencies:\n        return \"*\"\n    if not optional_dependencies:\n        return None\n\n    available_groups = _collect_available_optional_dependency_groups(\n        found_files,\n    )\n    missing_groups = [\n        group_name\n        for group_name in dict.fromkeys(optional_dependencies)\n        if group_name not in available_groups\n    ]\n    if missing_groups:\n        missing = \", \".join(f\"`{group_name}`\" for group_name in missing_groups)\n        if available_groups:\n            available = \", \".join(f\"`{group_name}`\" for group_name in available_groups)\n            print(\n                \"❌ Unknown optional dependency group(s): \"\n                f\"{missing}. Valid groups: {available}.\",\n            )\n        else:\n            print(\n                \"❌ Unknown optional dependency group(s): \"\n                f\"{missing}. No optional dependency groups were found.\",\n            )\n        sys.exit(1)\n\n    selected_groups = list(dict.fromkeys(optional_dependencies))\n    return [selected_groups.copy() for _ in found_files]\n\n\ndef _collect_selected_conda_like_platforms(\n    entries: list[DependencyEntry],\n) -> list[Platform]:\n    \"\"\"Collect all platforms referenced directly by dependency selectors.\"\"\"\n    selector_platforms: set[Platform] = set()\n    for entry in entries:\n        for spec in (entry.conda, entry.pip):\n            if spec is None or spec.selector is None:\n                continue\n            entry_platforms = spec.platforms()\n            if entry_platforms is not None:\n                selector_platforms.update(entry_platforms)\n    return sorted(selector_platforms)\n\n\ndef _add_common_args(  # noqa: PLR0912, C901\n    sub_parser: argparse.ArgumentParser,\n    options: set[str],\n) -> None:  # pragma: no cover\n    if \"directory\" in options:\n        sub_parser.add_argument(\n            \"-d\",\n            \"--directory\",\n            type=Path,\n            default=\".\",\n            help=f\"Base directory to scan for {_DEP_FILES} file(s), by default `.`\",\n        )\n    if \"depth\" in options:\n        sub_parser.add_argument(\n            \"--depth\",\n            type=int,\n            default=1,\n            help=f\"Maximum depth to scan for {_DEP_FILES} files, by default 1\",\n        )\n    if \"file\" in options or \"file-alt\" in options:\n        if \"file-alt\" in options:\n            help_msg = (\n                f\"A single {_DEP_FILES} file to use, or\"\n                \" folder that contains that file. This is an alternative to using\"\n                f\" `--directory` which searches for all {_DEP_FILES} files in the\"\n                \" directory and its subdirectories.\"\n            )\n        else:\n            help_msg = (\n                f\"The {_DEP_FILES} file to parse, or folder\"\n                \" that contains that file, by default `.`\"\n            )\n        assert \"conda-lock-file\" not in options  # both use \"-f\"\n        sub_parser.add_argument(\n            \"-f\",\n            \"--file\",\n            type=Path,\n            default=[],\n            action=\"append\",\n            help=help_msg,\n        )\n    if \"*files\" in options:\n        sub_parser.add_argument(\n            \"files\",\n            type=Path,\n            nargs=\"+\",\n            help=f\"The {_DEP_FILES} file(s) to parse\"\n            \" or folder(s) that contain those file(s), by default `.`\",\n            default=None,\n        )\n    if \"verbose\" in options:\n        sub_parser.add_argument(\n            \"-v\",\n            \"--verbose\",\n            action=\"store_true\",\n            help=\"Print verbose output\",\n        )\n    if \"platform\" in options:\n        sub_parser.add_argument(\n            \"-p\",\n            \"--platform\",\n            type=str,\n            action=\"append\",  # Allow multiple instances of -p\n            default=[],\n            choices=get_args(Platform),\n            help=\"The platform(s) to get the requirements for. \"\n            \"Multiple platforms can be specified. \"\n            \"If omitted, behavior is command-specific: platforms may be inferred \"\n            \"from requirements files, otherwise the current platform is used.\",\n        )\n    if \"editable\" in options:\n        sub_parser.add_argument(\n            \"-e\",\n            \"--editable\",\n            action=\"store_true\",\n            help=\"Install the project in editable mode\",\n        )\n    if \"skip-local\" in options:\n        sub_parser.add_argument(\n            \"--skip-local\",\n            action=\"store_true\",\n            help=\"Skip installing local dependencies\",\n        )\n    if \"skip-pip\" in options:\n        sub_parser.add_argument(\n            \"--skip-pip\",\n            action=\"store_true\",\n            help=f\"Skip installing pip dependencies from {_DEP_FILES}\",\n        )\n    if \"skip-conda\" in options:\n        sub_parser.add_argument(\n            \"--skip-conda\",\n            action=\"store_true\",\n            help=f\"Skip installing conda dependencies from {_DEP_FILES}\",\n        )\n    if \"skip-dependency\" in options:\n        sub_parser.add_argument(\n            \"--skip-dependency\",\n            type=str,\n            action=\"append\",\n            default=[],\n            help=\"Skip installing a specific dependency that is in one of the\"\n            f\" {_DEP_FILES}\"\n            \" files. This option can be used multiple times, each\"\n            \" time specifying a different package to skip.\"\n            \" For example, use `--skip-dependency pandas` to skip installing pandas.\",\n        )\n    if \"no-dependencies\" in options:\n        sub_parser.add_argument(\n            \"--no-dependencies\",\n            \"--no-deps\",\n            action=\"store_true\",\n            help=f\"Skip installing dependencies from {_DEP_FILES}\"\n            \" file(s) and only install local package(s). Useful after\"\n            \" installing a `conda-lock.yml` file because then all\"\n            \" dependencies have already been installed.\",\n        )\n    if \"conda-executable\" in options:\n        sub_parser.add_argument(\n            \"--conda-executable\",\n            type=str,\n            choices=(\"conda\", \"mamba\", \"micromamba\"),\n            help=\"The conda executable to use\",\n            default=None,\n        )\n    if \"conda-env\" in options:\n        grp = sub_parser.add_mutually_exclusive_group()\n        grp.add_argument(\n            \"-n\",\n            \"--conda-env-name\",\n            type=str,\n            default=None,\n            help=\"Name of the conda environment, if not provided, the currently\"\n            \" active environment name is used, unless `--conda-env-prefix` is\"\n            \" provided\",\n        )\n        grp.add_argument(\n            \"-p\",  # Overlaps with `--platform`, but that's fine\n            \"--conda-env-prefix\",\n            type=Path,\n            default=None,\n            help=\"Path to the conda environment, if not provided, the currently\"\n            \" active environment path is used, unless `--conda-env-name` is\"\n            \" provided\",\n        )\n    if \"dry-run\" in options:\n        sub_parser.add_argument(\n            \"--dry-run\",\n            \"--dry\",\n            action=\"store_true\",\n            help=\"Only print the commands that would be run\",\n        )\n    if \"ignore-pin\" in options:\n        sub_parser.add_argument(\n            \"--ignore-pin\",\n            type=str,\n            action=\"append\",\n            default=[],\n            help=\"Ignore the version pin for a specific package,\"\n            \" e.g., `--ignore-pin numpy`. This option can be repeated\"\n            \" to ignore multiple packages.\",\n        )\n    if \"overwrite-pin\" in options:\n        sub_parser.add_argument(\n            \"--overwrite-pin\",\n            type=str,\n            action=\"append\",\n            default=[],\n            help=\"Overwrite the version pin for a specific package,\"\n            \" e.g., `--overwrite-pin 'numpy=1.19.2'`. This option can be repeated\"\n            \" to overwrite the pins of multiple packages.\",\n        )\n    if \"conda-lock-file\" in options:\n        sub_parser.add_argument(\n            \"-f\",\n            \"--conda-lock-file\",\n            type=Path,\n            help=\"Path to the `conda-lock.yml` file to use for creating the new\"\n            \" environment. Assumes that the lock file contains all dependencies.\"\n            \" Must be used with `--conda-env-name` or `--conda-env-prefix`.\",\n        )\n    if \"no-uv\" in options:\n        sub_parser.add_argument(\n            \"--no-uv\",\n            action=\"store_true\",\n            help=\"Disables the use of `uv` for pip install. By default, `uv` is used\"\n            \" if it is available in the PATH.\",\n        )\n\n\ndef _add_extra_flags(\n    subparser: argparse.ArgumentParser,\n    downstream_command: str,\n    unidep_subcommand: str,\n    example: str,\n) -> None:\n    subparser.add_argument(\n        \"extra_flags\",\n        nargs=argparse.REMAINDER,\n        help=f\"Extra flags to pass to `{downstream_command}`. These flags are passed\"\n        f\" directly and should be provided in the format expected by\"\n        f\" `{downstream_command}`. For example, `unidep {unidep_subcommand} -- {example}`.\"  # noqa: E501\n        f\" Note that the `--` is required to separate the flags for\"\n        f\" `unidep {unidep_subcommand}` from the flags for `{downstream_command}`.\",\n    )\n\n\ndef _parse_args() -> argparse.Namespace:  # noqa: PLR0915\n    parser = argparse.ArgumentParser(\n        description=\"Unified Conda and Pip requirements management.\",\n        formatter_class=_HelpFormatter,\n    )\n    subparsers = parser.add_subparsers(dest=\"command\", help=\"Subcommands\")\n\n    # Subparser for the 'merge' command\n    merge_help = (\n        f\"Combine multiple (or a single) {_DEP_FILES}\"\n        \" files into a\"\n        \" single Conda installable `environment.yaml` file.\"\n    )\n    merge_example = (\n        \" Example usage: `unidep merge --directory . --depth 1 --output environment.yaml`\"  # noqa: E501\n        f\" to search for {_DEP_FILES}\"\n        \" files in the current directory and its\"\n        \" subdirectories and create `environment.yaml`. These are the defaults, so you\"\n        \" can also just run `unidep merge`. For Pixi support, use `unidep pixi`.\"\n    )\n    parser_merge = subparsers.add_parser(\n        \"merge\",\n        help=merge_help,\n        description=merge_help + merge_example,\n        formatter_class=_HelpFormatter,\n    )\n    parser_merge.add_argument(\n        \"-o\",\n        \"--output\",\n        type=Path,\n        default=None,\n        help=\"Output file for the conda environment, by default `environment.yaml`\",\n    )\n    parser_merge.add_argument(\n        \"-n\",\n        \"--name\",\n        type=str,\n        default=\"myenv\",\n        help=\"Name of the conda environment, by default `myenv`\",\n    )\n    parser_merge.add_argument(\n        \"--stdout\",\n        action=\"store_true\",\n        help=\"Output to stdout instead of a file\",\n    )\n    parser_merge.add_argument(\n        \"--selector\",\n        type=str,\n        choices=(\"sel\", \"comment\"),\n        default=\"sel\",\n        help=\"The selector to use for the environment markers, if `sel` then\"\n        \" `- numpy # [linux]` becomes `sel(linux): numpy`, if `comment` then\"\n        \" it remains `- numpy # [linux]`, by default `sel`\",\n    )\n    merge_optional_group = parser_merge.add_mutually_exclusive_group()\n    merge_optional_group.add_argument(\n        \"--optional-dependencies\",\n        nargs=\"+\",\n        metavar=\"GROUP\",\n        default=[],\n        help=\"Include the named optional dependency group(s) from the discovered\"\n        \" requirements files.\",\n    )\n    merge_optional_group.add_argument(\n        \"--all-optional-dependencies\",\n        action=\"store_true\",\n        help=\"Include all optional dependency groups from the discovered\"\n        \" requirements files.\",\n    )\n    _add_common_args(\n        parser_merge,\n        {\n            \"directory\",\n            \"verbose\",\n            \"platform\",\n            \"depth\",\n            \"ignore-pin\",\n            \"skip-dependency\",\n            \"overwrite-pin\",\n        },\n    )\n\n    # Subparser for the 'install' command\n    install_help = (\n        f\"Automatically install all dependencies from one or more {_DEP_FILES} files.\"\n        \" This command first installs dependencies\"\n        \" with Conda, then with Pip. Finally, it installs local packages\"\n        f\" (those containing the {_DEP_FILES} files)\"\n        \" using `pip install [-e] ./project`.\"\n    )\n    install_example = (\n        \" Example usage: `unidep install .` for a single project.\"\n        \" For multiple projects: `unidep install ./project1 ./project2`.\"\n        \" The command accepts both file paths and directories containing\"\n        f\" a {_DEP_FILES} file. Use `--editable` or\"\n        \" `-e` to install the local packages in editable mode. See\"\n        f\" `unidep install-all` to install all {_DEP_FILES} files in and below the\"\n        \" current folder.\"\n    )\n\n    parser_install = subparsers.add_parser(\n        \"install\",\n        help=install_help,\n        description=install_help + install_example,\n        formatter_class=_HelpFormatter,\n    )\n\n    # Add positional argument for the file\n    _add_common_args(\n        parser_install,\n        {\n            \"*files\",\n            \"conda-executable\",\n            \"conda-env\",\n            \"conda-lock-file\",\n            \"dry-run\",\n            \"editable\",\n            \"skip-local\",\n            \"skip-pip\",\n            \"skip-conda\",\n            \"no-dependencies\",\n            \"ignore-pin\",\n            \"skip-dependency\",\n            \"overwrite-pin\",\n            \"no-uv\",\n            \"verbose\",\n        },\n    )\n    install_all_help = (\n        f\"Install dependencies from all {_DEP_FILES}\"\n        \" files found in the current\"\n        \" directory or specified directory. This command first installs dependencies\"\n        \" using Conda, then Pip, and finally the local packages.\"\n    )\n    install_all_example = (\n        \" Example usage: `unidep install-all` to install dependencies from all\"\n        f\" {_DEP_FILES}\"\n        \" files in the current directory. Use\"\n        \" `--directory ./path/to/dir` to specify a different directory. Use\"\n        \" `--depth` to control the depth of directory search. Add `--editable`\"\n        \" or `-e` for installing local packages in editable mode.\"\n    )\n\n    parser_install_all = subparsers.add_parser(\n        \"install-all\",\n        help=install_all_help,\n        description=install_all_help + install_all_example,\n        formatter_class=_HelpFormatter,\n    )\n\n    # Add positional argument for the file\n    _add_common_args(\n        parser_install_all,\n        {\n            \"conda-executable\",\n            \"conda-env\",\n            \"conda-lock-file\",\n            \"dry-run\",\n            \"editable\",\n            \"depth\",\n            \"directory\",\n            \"skip-local\",\n            \"skip-pip\",\n            \"skip-conda\",\n            \"no-dependencies\",\n            \"ignore-pin\",\n            \"skip-dependency\",\n            \"overwrite-pin\",\n            \"no-uv\",\n            \"verbose\",\n        },\n    )\n\n    # Subparser for the 'conda-lock' command\n\n    conda_lock_help = (\n        \"Generate a global `conda-lock.yml` file for a collection of\"\n        f\" {_DEP_FILES}\"\n        \" files. Additionally, create individual\"\n        f\" `conda-lock.yml` files for each {_DEP_FILES} file\"\n        \" consistent with the global lock file.\"\n    )\n    conda_lock_example = (\n        \" Example usage: `unidep conda-lock --directory ./projects` to generate\"\n        f\" conda-lock files for all {_DEP_FILES}\"\n        \" files in the `./projects`\"\n        \" directory. Use `--only-global` to generate only the global lock file.\"\n        \" The `--check-input-hash` option can be used to avoid regenerating lock\"\n        \" files if the input hasn't changed.\"\n    )\n\n    parser_lock = subparsers.add_parser(\n        \"conda-lock\",\n        help=conda_lock_help,\n        description=conda_lock_help + conda_lock_example,\n        formatter_class=_HelpFormatter,\n    )\n\n    parser_lock.add_argument(\n        \"--only-global\",\n        action=\"store_true\",\n        help=\"Only generate the global lock file\",\n    )\n    parser_lock.add_argument(\n        \"--lockfile\",\n        type=Path,\n        default=\"conda-lock.yml\",\n        help=\"Specify a path for the global lockfile (default: `conda-lock.yml`\"\n        \" in current directory). Path should be relative, e.g.,\"\n        \" `--lockfile ./locks/example.conda-lock.yml`.\",\n    )\n    parser_lock.add_argument(\n        \"--check-input-hash\",\n        action=\"store_true\",\n        help=\"Check existing input hashes in lockfiles before regenerating lock files.\"\n        \" This flag is directly passed to `conda-lock`.\",\n    )\n    _add_common_args(\n        parser_lock,\n        {\n            \"directory\",\n            \"file-alt\",\n            \"verbose\",\n            \"platform\",\n            \"depth\",\n            \"ignore-pin\",\n            \"skip-dependency\",\n            \"overwrite-pin\",\n        },\n    )\n    _add_extra_flags(parser_lock, \"conda-lock lock\", \"conda-lock\", \"--micromamba\")\n\n    # Subparser for the 'pixi' command\n    pixi_help = f\"Generate a `pixi.toml` file from {_DEP_FILES} files.\"\n    pixi_example = (\n        \" Example usage: `unidep pixi` to generate a pixi.toml file. \"\n        \"Use `--output` to specify a different output path. \"\n        \"Use `--name` to set the project name. \"\n        \"After generating, use `pixi lock` and `pixi install` directly.\"\n    )\n    parser_pixi = subparsers.add_parser(\n        \"pixi\",\n        help=pixi_help,\n        description=pixi_help + pixi_example,\n        formatter_class=_HelpFormatter,\n    )\n    parser_pixi.add_argument(\n        \"-o\",\n        \"--output\",\n        type=Path,\n        default=None,\n        help=\"Output path for pixi.toml (default: pixi.toml in current directory)\",\n    )\n    parser_pixi.add_argument(\n        \"-n\",\n        \"--name\",\n        type=str,\n        default=None,\n        help=\"Name of the project (default: current directory name)\",\n    )\n    parser_pixi.add_argument(\n        \"--stdout\",\n        action=\"store_true\",\n        help=\"Output to stdout instead of a file\",\n    )\n    parser_pixi.add_argument(\n        \"-c\",\n        \"--channel\",\n        action=\"append\",\n        help=\"Conda channel to include. Can be repeated. Overrides channels\"\n        \" declared in requirements files. If omitted, channels are read from\"\n        \" the requirements files (defaulting to conda-forge).\",\n    )\n    _add_common_args(\n        parser_pixi,\n        {\n            \"directory\",\n            \"file-alt\",\n            \"verbose\",\n            \"platform\",\n            \"depth\",\n            \"ignore-pin\",\n            \"skip-dependency\",\n            \"overwrite-pin\",\n        },\n    )\n\n    # Subparser for the 'pip-compile' command\n    pip_compile_help = (\n        \"Generate a fully pinned `requirements.txt` file from one or more\"\n        f\" {_DEP_FILES}\"\n        \" files using `pip-compile` from `pip-tools`. This\"\n        f\" command consolidates all pip dependencies defined in the {_DEP_FILES}\"\n        \" files and compiles them into a single `requirements.txt` file, taking\"\n        \" into account the specific versions and dependencies of each package.\"\n    )\n    pip_compile_example = (\n        \" Example usage: `unidep pip-compile --directory ./projects` to generate\"\n        f\" a `requirements.txt` file for all {_DEP_FILES}\"\n        \" files in the\"\n        \" `./projects` directory. Use `--output-file requirements.txt` to specify a\"\n        \" different output file.\"\n    )\n\n    parser_pip_compile = subparsers.add_parser(\n        \"pip-compile\",\n        help=pip_compile_help,\n        description=pip_compile_help + pip_compile_example,\n        formatter_class=_HelpFormatter,\n    )\n    parser_pip_compile.add_argument(\n        \"-o\",\n        \"--output-file\",\n        type=Path,\n        default=None,\n        help=\"Output file for the pip requirements, by default `requirements.txt`\",\n    )\n    _add_common_args(\n        parser_pip_compile,\n        {\n            \"directory\",\n            \"verbose\",\n            \"platform\",\n            \"depth\",\n            \"ignore-pin\",\n            \"skip-dependency\",\n            \"overwrite-pin\",\n        },\n    )\n    _add_extra_flags(\n        parser_pip_compile,\n        \"pip-compile\",\n        \"pip-compile\",\n        \"--generate-hashes --allow-unsafe\",\n    )\n\n    # Subparser for the 'pip' and 'conda' command\n    help_str = \"Get the {} requirements for the current platform only.\"\n    help_example = (\n        \" Example usage: `unidep {which} --file folder1 --file\"\n        \" folder2/requirements.yaml --separator ' ' --platform linux-64` to\"\n        \" extract all the {which} dependencies specific to the linux-64 platform. Note\"\n        \" that the `--file` argument can be used multiple times to specify multiple\"\n        f\" {_DEP_FILES}\"\n        \" files and that --file can also be a folder that contains\"\n        f\" a {_DEP_FILES} file.\"\n    )\n    parser_pip = subparsers.add_parser(\n        \"pip\",\n        help=help_str.format(\"pip\"),\n        description=help_str.format(\"pip\") + help_example.format(which=\"pip\"),\n        formatter_class=_HelpFormatter,\n    )\n    parser_conda = subparsers.add_parser(\n        \"conda\",\n        help=help_str.format(\"conda\"),\n        description=help_str.format(\"conda\") + help_example.format(which=\"conda\"),\n        formatter_class=_HelpFormatter,\n    )\n    for sub_parser in [parser_pip, parser_conda]:\n        _add_common_args(\n            sub_parser,\n            {\n                \"verbose\",\n                \"platform\",\n                \"file\",\n                \"ignore-pin\",\n                \"skip-dependency\",\n                \"overwrite-pin\",\n            },\n        )\n        sub_parser.add_argument(\n            \"--separator\",\n            type=str,\n            default=\" \",\n            help=\"The separator between the dependencies, by default ` `\",\n        )\n\n    # Subparser for the 'version' command\n    parser_merge = subparsers.add_parser(\n        \"version\",\n        help=\"Print version information of unidep.\",\n        formatter_class=_HelpFormatter,\n    )\n\n    args = parser.parse_args()\n\n    if args.command is None:  # pragma: no cover\n        parser.print_help()\n        sys.exit(1)\n\n    if \"file\" in args:\n        _ensure_files(args.file)\n\n    return args\n\n\ndef _ensure_files(files: list[Path]) -> None:\n    \"\"\"Ensure that the files exist.\"\"\"\n    missing = []\n    for i, f in enumerate(files):\n        try:\n            path_with_extras = parse_folder_or_filename(f)\n        except FileNotFoundError:  # noqa: PERF203\n            missing.append(f\"`{f}`\")\n        else:\n            files[i] = path_with_extras.path_with_extras\n    if missing:\n        print(f\"❌ One or more files ({', '.join(missing)}) not found.\")\n        sys.exit(1)\n\n\ndef _get_conda_executable(which: CondaExecutable) -> str | None:\n    if shutil.which(which):\n        return which  # Found in PATH so return the name\n    # e.g., micromamba might be a bash function, check env var in that case\n    env_var = \"CONDA_EXE\" if which == \"conda\" else \"MAMBA_EXE\"\n    exe = os.environ.get(env_var, None)\n    if exe is None:  # pragma: no cover\n        return None\n    if Path(exe).name != which:  # pragma: no cover\n        return None\n    return exe\n\n\ndef _identify_conda_executable() -> CondaExecutable:  # pragma: no cover\n    \"\"\"Identify the conda executable to use.\n\n    This function checks for micromamba, mamba, and conda in that order.\n    \"\"\"\n    if _get_conda_executable(\"micromamba\") is not None:\n        return \"micromamba\"\n    if _get_conda_executable(\"mamba\") is not None:\n        return \"mamba\"\n    if _get_conda_executable(\"conda\") is not None:\n        return \"conda\"\n    msg = \"Could not identify conda executable.\"\n    raise RuntimeError(msg)\n\n\ndef _maybe_conda_executable() -> CondaExecutable | None:\n    try:\n        return _identify_conda_executable()\n    except RuntimeError:  # pragma: no cover\n        return None\n\n\ndef _format_inline_conda_package(package: str) -> str:\n    pkg = parse_package_str(package)\n    if pkg.pin is None:\n        return pkg.name\n    return f'{pkg.name}\"{pkg.pin.strip()}\"'\n\n\ndef _maybe_exe(conda_executable: CondaExecutable) -> str:\n    \"\"\"Add .exe on Windows.\"\"\"\n    if os.name == \"nt\":  # pragma: no cover\n        if conda_executable in (\"micromamba\", \"mamba\") and os.environ.get(\"MAMBA_EXE\"):\n            return os.path.normpath(os.environ[\"MAMBA_EXE\"])\n        if os.environ.get(\"CONDA_EXE\"):\n            return os.path.normpath(os.environ[\"CONDA_EXE\"])\n\n        executables = [f\"{conda_executable}.exe\", conda_executable]\n        for exe in executables:\n            path = shutil.which(exe)\n            if path is not None:\n                return os.path.normpath(path)\n\n        print(\n            \"🔍 Going to search in different common paths\"\n            f\" because `{conda_executable}` was not found in PATH.\",\n        )\n        return os.path.normpath(_find_windows_path(conda_executable))\n    executable = _get_conda_executable(conda_executable)\n    assert executable is not None\n    return executable\n\n\ndef _capitalize_dir(path: str, *, capitalize: bool = True, index: int = -1) -> str:\n    \"\"\"Capitalize or lowercase a directory in a path, on Windows only.\"\"\"\n    sep = \"\\\\\"\n    parts = path.split(sep)\n    if capitalize:\n        parts[index] = parts[index].capitalize()\n    else:\n        parts[index] = parts[index].lower()\n    return sep.join(parts)\n\n\n@functools.lru_cache(1)\ndef _find_windows_path(conda_executable: CondaExecutable) -> str:\n    \"\"\"Find the path to the conda executable on Windows.\"\"\"\n    searched = []\n    conda_roots = [\n        r\"%USERPROFILE%\\Anaconda3\",  # https://stackoverflow.com/a/58211115\n        r\"%USERPROFILE%\\Miniconda3\",  # https://stackoverflow.com/a/76545804\n        r\"C:\\Anaconda3\",  # https://stackoverflow.com/a/44597801\n        r\"C:\\Miniconda3\",  # https://stackoverflow.com/a/53685910\n        r\"C:\\ProgramData\\Anaconda3\",  # https://stackoverflow.com/a/58211115\n        r\"C:\\ProgramData\\Miniconda3\",  # https://stackoverflow.com/a/51003321\n    ]\n    if conda_executable == \"mamba\":\n        conda_roots = [\n            r\"C:\\ProgramData\\mambaforge\",  # https://github.com/mamba-org/mamba/issues/1756#issuecomment-1517284831\n            r\"%USERPROFILE%\\AppData\\Local\\mambaforge\",  # https://stackoverflow.com/a/75612393\n            # First try native mamba locations, then Conda locations (in\n            # case `conda install mamba` was used)\n            *conda_roots,\n        ]\n    if conda_executable == \"micromamba\":\n        conda_roots = [\n            # Default installation directory based on the installation script\n            # https://raw.githubusercontent.com/mamba-org/micromamba-releases/main/install.ps1\n            r\"%LOCALAPPDATA%\\micromamba\",\n        ]\n\n    extensions = (\".exe\", \"\", \".bat\")\n    subs = (\"condabin\\\\\", \"Scripts\\\\\", \"\")  # The \"\" is for micromamba\n    for root, sub, ext, cap in itertools.product(\n        conda_roots,\n        subs,\n        extensions,\n        (True, False),\n    ):\n        # @sbalk reported that his `anaconda3` folder is lowercase\n        path = rf\"{_capitalize_dir(root, capitalize=cap)}\\{sub}{conda_executable}{ext}\"\n        path = os.path.expandvars(path)\n        searched.append(path)\n        if os.path.exists(path):  # noqa: PTH110\n            return path\n    msg = f\"Could not find {conda_executable}.\"\n    searched_str = \"\\n👉 \".join(searched)\n    msg = f\"Could not find {conda_executable}. Searched in:\\n👉 {searched_str}\"\n    raise FileNotFoundError(msg)\n\n\ndef _conda_cli_command_json(\n    conda_executable: CondaExecutable,\n    *args: str,\n) -> dict[str, list[str]]:\n    \"\"\"Run a conda command and return the JSON output.\"\"\"\n    try:\n        result = subprocess.run(\n            [_maybe_exe(conda_executable), *args, \"--json\"],\n            capture_output=True,\n            text=True,\n            check=True,\n        )\n        return json.loads(result.stdout)\n    except subprocess.CalledProcessError as e:  # pragma: no cover\n        print(f\"Error occurred: {e}\")\n        raise\n    except json.JSONDecodeError as e:  # pragma: no cover\n        print(f\"Failed to parse JSON: {e}\")\n        raise\n\n\n@functools.lru_cache(maxsize=None)\ndef _conda_env_list(conda_executable: CondaExecutable) -> list[str]:\n    \"\"\"Get a list of conda environments.\"\"\"\n    return _conda_cli_command_json(conda_executable, \"env\", \"list\")[\"envs\"]\n\n\n@functools.lru_cache(maxsize=None)\ndef _conda_info(conda_executable: CondaExecutable) -> dict:\n    return _conda_cli_command_json(conda_executable, \"info\")\n\n\ndef _conda_root_prefix(conda_executable: CondaExecutable) -> Path:  # pragma: no cover\n    \"\"\"Get the root prefix of the conda installation.\"\"\"\n    if os.environ.get(\"MAMBA_ROOT_PREFIX\"):\n        return Path(os.environ[\"MAMBA_ROOT_PREFIX\"])\n    if os.environ.get(\"CONDA_ROOT\"):\n        return Path(os.environ[\"CONDA_ROOT\"])\n    info_dict = _conda_info(conda_executable)\n    if conda_executable in (\"conda\", \"mamba\"):\n        prefix = info_dict.get(\"root_prefix\") or info_dict[\"conda_prefix\"]\n    else:\n        assert conda_executable == \"micromamba\"\n        prefix = info_dict[\"base environment\"]\n    return Path(prefix)\n\n\ndef _conda_env_dirs(\n    conda_executable: CondaExecutable,\n) -> list[Path]:  # pragma: no cover\n    \"\"\"Get a list of conda environment directories.\"\"\"\n    info_dict = _conda_info(conda_executable)\n    if conda_executable in (\"conda\", \"mamba\"):\n        envs_dirs = info_dict[\"envs_dirs\"]\n    else:\n        assert conda_executable == \"micromamba\"\n        envs_dirs = info_dict[\"envs directories\"]\n    return [Path(d) for d in envs_dirs]\n\n\ndef _conda_env_name_to_prefix(\n    conda_executable: CondaExecutable,\n    conda_env_name: str,\n    *,\n    raise_if_not_found: bool = True,\n) -> Path | None:  # pragma: no cover\n    \"\"\"Get the prefix of a conda environment.\"\"\"\n    # Based on `conda.base.context.locate_prefix_by_name`\n    # https://github.com/conda/conda/blob/72fe69dac8b2fef351c511c813493fef17f295e1/conda/base/context.py#L1976-L1977\n    root_prefix = _conda_root_prefix(conda_executable)\n    if conda_env_name in (\"base\", \"root\"):\n        return root_prefix\n\n    for envs_dir in _conda_env_dirs(conda_executable):\n        prefix = envs_dir / conda_env_name\n        if prefix.exists():\n            return prefix\n    if not raise_if_not_found:\n        return None\n    envs = _conda_env_list(conda_executable)\n    envs_str = \"\\n👉 \".join(envs)\n    msg = (\n        f\"Could not find conda prefix with name `{conda_env_name}`.\"\n        f\" Available prefixes:\\n👉 {envs_str}\"\n    )\n    raise ValueError(msg)\n\n\ndef _maybe_create_conda_env_args(\n    conda_executable: CondaExecutable,\n    conda_env_name: str | None,\n    conda_env_prefix: Path | None,\n) -> list[str]:\n    if not conda_env_name and not conda_env_prefix:\n        return []\n    conda_env_args = []\n    if conda_env_name:\n        conda_env_args = [\"--name\", conda_env_name]\n        prefix = _conda_env_name_to_prefix(\n            conda_executable,\n            conda_env_name,\n            raise_if_not_found=False,\n        )\n        if prefix is None:\n            _create_conda_environment(conda_executable, *conda_env_args)\n    elif conda_env_prefix:\n        conda_env_args = [\"--prefix\", str(conda_env_prefix)]\n        if not conda_env_prefix.exists():\n            _create_conda_environment(conda_executable, *conda_env_args)\n    return conda_env_args\n\n\ndef _create_conda_environment(\n    conda_executable: CondaExecutable,\n    *args: str,\n) -> None:  # pragma: no cover\n    \"\"\"Create an empty conda environment.\"\"\"\n    conda_command = [_maybe_exe(conda_executable), \"create\", \"--yes\", *args]\n    print(f\"📦 Creating empty conda environment with `{' '.join(conda_command)}`\\n\")\n    subprocess.run(conda_command, check=True)\n\n\ndef _python_executable(\n    conda_executable: CondaExecutable | None,\n    conda_env_name: str | None,\n    conda_env_prefix: Path | None,\n) -> str:\n    \"\"\"Get the Python executable to use for a conda environment.\"\"\"\n    if conda_env_name is None and conda_env_prefix is None:\n        return sys.executable\n    if conda_env_name:\n        assert conda_executable is not None\n        conda_env_prefix = _conda_env_name_to_prefix(conda_executable, conda_env_name)\n    assert conda_env_prefix is not None\n    if platform.system() == \"Windows\":  # pragma: no cover\n        python_executable = conda_env_prefix / \"python.exe\"\n    else:\n        python_executable = conda_env_prefix / \"bin\" / \"python\"\n    assert python_executable.exists()\n    return str(python_executable)\n\n\ndef _use_uv(no_uv: bool) -> bool:  # noqa: FBT001\n    \"\"\"Check if the user wants to use the `uv` package.\"\"\"\n    if no_uv:\n        return False\n    return shutil.which(\"uv\") is not None\n\n\ndef _build_pip_index_arguments(pip_indices: Sequence[str]) -> list[str]:\n    \"\"\"Build pip/uv index arguments from pip_indices list.\n\n    First index becomes --index-url (primary),\n    remaining indices become --extra-index-url (supplementary).\n    \"\"\"\n    args = []\n    if pip_indices:\n        # Expand environment variables in URLs\n        expanded_indices = []\n        for index in pip_indices:\n            expanded = os.path.expandvars(index)\n            expanded_indices.append(expanded)\n\n        # First index is primary\n        args.extend([\"--index-url\", expanded_indices[0]])\n        # Additional indices are extra\n        for index in expanded_indices[1:]:\n            args.extend([\"--extra-index-url\", index])\n    return args\n\n\ndef _pip_install_local(\n    *folders: str | Path,\n    editable: bool,\n    dry_run: bool,\n    python_executable: str,\n    conda_run: list[str],\n    no_uv: bool,\n    pip_indices: Sequence[str] | None = None,\n    flags: list[str] | None = None,\n) -> None:  # pragma: no cover\n    index_args = _build_pip_index_arguments(pip_indices or [])\n    if _use_uv(no_uv):\n        pip_command = [\n            *conda_run,\n            \"uv\",\n            \"pip\",\n            \"install\",\n            \"--python\",\n            python_executable,\n            *index_args,\n        ]\n    else:\n        pip_command = [\n            *conda_run,\n            python_executable,\n            \"-m\",\n            \"pip\",\n            \"install\",\n            *index_args,\n        ]\n\n    if flags:\n        pip_command.extend(flags)\n\n    for folder in sorted(folders):\n        if not os.path.isabs(folder):  # noqa: PTH117\n            relative_prefix = \".\\\\\" if os.name == \"nt\" else \"./\"\n            folder = f\"{relative_prefix}{folder}\"  # noqa: PLW2901\n\n        if (\n            editable\n            and not str(folder).endswith(\".whl\")\n            and not str(folder).endswith(\".zip\")\n        ):\n            pip_command.extend([\"-e\", str(folder)])\n        else:\n            pip_command.append(str(folder))\n\n    print(f\"📦 Installing project with `{' '.join(pip_command)}`\\n\")\n    if not dry_run:\n        subprocess.run(pip_command, check=True)\n\n\ndef _install_command(  # noqa: C901, PLR0912, PLR0915\n    *files: Path,\n    conda_executable: CondaExecutable | None,\n    conda_env_name: str | None,\n    conda_env_prefix: Path | None,\n    conda_lock_file: Path | None,\n    dry_run: bool,\n    editable: bool,\n    skip_local: bool = False,\n    skip_pip: bool = False,\n    skip_conda: bool = False,\n    no_dependencies: bool = False,\n    ignore_pins: list[str] | None = None,\n    overwrite_pins: list[str] | None = None,\n    skip_dependencies: list[str] | None = None,\n    no_uv: bool = True,\n    verbose: bool = False,\n) -> None:\n    \"\"\"Install the dependencies of a single `requirements.yaml` or `pyproject.toml` file.\"\"\"  # noqa: E501\n    start_time = time.time()\n    paths_with_extras = [parse_folder_or_filename(f) for f in files]\n    requirements = parse_requirements(\n        *[f.path for f in paths_with_extras],\n        ignore_pins=ignore_pins,\n        overwrite_pins=overwrite_pins,\n        skip_dependencies=skip_dependencies,\n        verbose=verbose,\n        extras=[f.extras for f in paths_with_extras],\n    )\n    platforms = [identify_current_platform()]\n    env_entries = _flatten_selected_dependency_entries(\n        requirements.dependency_entries,\n        requirements.optional_dependency_entries,\n    )\n    env_spec = create_conda_env_specification(\n        env_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        platforms=platforms,\n    )\n    if not conda_executable:  # None or empty string\n        conda_executable = _maybe_conda_executable()\n    if conda_lock_file:  # As late as possible to error out early in previous steps\n        assert conda_executable is not None\n        _create_env_from_lock(\n            conda_lock_file,\n            conda_executable,\n            conda_env_name=conda_env_name,\n            conda_env_prefix=conda_env_prefix,\n            dry_run=dry_run,\n            verbose=verbose,\n        )\n        no_dependencies = True  # Assume the lock file has all dependencies\n\n    if no_dependencies:\n        skip_pip = True\n        skip_conda = True\n\n    if env_spec.conda and not skip_conda:\n        assert conda_executable is not None\n        channel_args = [\"--override-channels\"] if env_spec.channels else []\n        for channel in env_spec.channels:\n            channel_args.extend([\"--channel\", channel])\n        conda_env_args = _maybe_create_conda_env_args(\n            conda_executable,\n            conda_env_name,\n            conda_env_prefix,\n        )\n        conda_command = [\n            _maybe_exe(conda_executable),\n            \"install\",\n            \"--yes\",\n            *channel_args,\n            *conda_env_args,\n        ]\n        # When running the command in terminal, we need to wrap the pin in quotes\n        # so what we print is what the user would type (copy-paste).\n        to_print = [_format_inline_conda_package(pkg) for pkg in env_spec.conda]  # type: ignore[arg-type]\n        conda_command_str = \" \".join((*conda_command, *to_print))\n        print(f\"📦 Installing conda dependencies with `{conda_command_str}`\\n\")  # type: ignore[arg-type]\n        if not dry_run:  # pragma: no cover\n            subprocess.run((*conda_command, *env_spec.conda), check=True)  # type: ignore[arg-type]\n    python_executable = _python_executable(\n        conda_executable,\n        conda_env_name,\n        conda_env_prefix,\n    )\n    if env_spec.pip and not skip_pip:\n        conda_run = _maybe_conda_run(conda_executable, conda_env_name, conda_env_prefix)\n        index_args = _build_pip_index_arguments(env_spec.pip_indices)\n        if _use_uv(no_uv):\n            pip_command = [\n                *conda_run,\n                \"uv\",\n                \"pip\",\n                \"install\",\n                \"--python\",\n                python_executable,\n                *index_args,\n                *env_spec.pip,\n            ]\n        else:\n            pip_command = [\n                *conda_run,\n                python_executable,\n                \"-m\",\n                \"pip\",\n                \"install\",\n                *index_args,\n                *env_spec.pip,\n            ]\n        print(f\"📦 Installing pip dependencies with `{' '.join(pip_command)}`\\n\")\n        if not dry_run:  # pragma: no cover\n            subprocess.run(pip_command, check=True)\n\n    installable = []\n    if not skip_local:\n        for file in paths_with_extras:\n            if is_pip_installable(file.path.parent):\n                installable.append(file.path.parent)\n            else:  # pragma: no cover\n                print(\n                    f\"⚠️  Project {file.path.parent} is not pip installable. \"\n                    \"Could not find setup.py or [build-system] in pyproject.toml.\",\n                )\n\n        # Install local dependencies (if any) included via `local_dependencies:`\n        local_dependencies = parse_local_dependencies(\n            *[p.path_with_extras for p in paths_with_extras],\n            check_pip_installable=True,\n            verbose=verbose,\n        )\n        names = {k.name: [dep.name for dep in v] for k, v in local_dependencies.items()}\n        print(f\"📝 Found local dependencies: {names}\\n\")\n        installable_set = {p.resolve() for p in installable}\n        for deps in local_dependencies.values():\n            for dep in deps:\n                resolved_dep = dep.resolve()\n                if resolved_dep in installable_set:\n                    continue\n                installable_set.add(resolved_dep)\n                installable.append(dep)\n        if installable:\n            pip_flags = [\"--no-deps\"]  # we just ran pip/conda install, so skip\n            if verbose:\n                pip_flags.append(\"--verbose\")\n            conda_run = _maybe_conda_run(\n                conda_executable,\n                conda_env_name,\n                conda_env_prefix,\n            )\n            _pip_install_local(\n                *sorted(installable),\n                editable=editable,\n                dry_run=dry_run,\n                python_executable=python_executable,\n                flags=pip_flags,\n                no_uv=no_uv,\n                pip_indices=env_spec.pip_indices,\n                conda_run=conda_run,\n            )\n\n    if not dry_run:  # pragma: no cover\n        total_time = time.time() - start_time\n        msg = f\"✅ All dependencies installed successfully in {total_time:.2f} seconds.\"\n        print(msg)\n\n\ndef _install_all_command(\n    *,\n    conda_executable: CondaExecutable | None,\n    conda_env_name: str | None,\n    conda_env_prefix: Path | None,\n    conda_lock_file: Path | None,\n    dry_run: bool,\n    editable: bool,\n    depth: int,\n    directory: Path,\n    skip_local: bool = False,\n    skip_pip: bool = False,\n    skip_conda: bool = False,\n    no_dependencies: bool = False,\n    ignore_pins: list[str] | None = None,\n    overwrite_pins: list[str] | None = None,\n    skip_dependencies: list[str] | None = None,\n    no_uv: bool = True,\n    verbose: bool = False,\n) -> None:  # pragma: no cover\n    found_files = find_requirements_files(\n        directory,\n        depth,\n        verbose=verbose,\n    )\n    if not found_files:\n        print(f\"❌ No {_DEP_FILES} files found in {directory}\")\n        sys.exit(1)\n    _install_command(\n        *found_files,\n        conda_executable=conda_executable,\n        conda_env_name=conda_env_name,\n        conda_env_prefix=conda_env_prefix,\n        conda_lock_file=conda_lock_file,\n        dry_run=dry_run,\n        editable=editable,\n        skip_local=skip_local,\n        skip_pip=skip_pip,\n        skip_conda=skip_conda,\n        no_dependencies=no_dependencies,\n        ignore_pins=ignore_pins,\n        overwrite_pins=overwrite_pins,\n        skip_dependencies=skip_dependencies,\n        no_uv=no_uv,\n        verbose=verbose,\n    )\n\n\ndef _maybe_conda_run(\n    conda_executable: CondaExecutable | None,\n    conda_env_name: str | None,\n    conda_env_prefix: Path | None,\n) -> list[str]:\n    if not conda_executable:  # None or empty string\n        return []\n    if conda_env_name is None and conda_env_prefix is None:\n        if not os.getenv(\"CONDA_PREFIX\") and not os.getenv(\"MAMBA_ROOT_PREFIX\"):\n            # Conda/mamba/micromamba might be installed but not in PATH\n            return []\n        exe = Path(sys.executable)\n        conda_prefix = exe.parent if os.name == \"nt\" else exe.parent.parent\n        env_args = [\"--prefix\", str(conda_prefix)]\n    elif conda_env_name:\n        env_args = [\"--name\", conda_env_name]\n    elif conda_env_prefix:\n        env_args = [\"--prefix\", str(conda_env_prefix)]\n    return [_maybe_exe(conda_executable), \"run\", *env_args]\n\n\ndef _create_env_from_lock(  # noqa: PLR0912\n    conda_lock_file: Path,\n    conda_executable: CondaExecutable,\n    conda_env_name: str | None,\n    conda_env_prefix: Path | None,\n    *,\n    dry_run: bool,\n    verbose: bool,\n) -> None:\n    if conda_env_name is None and conda_env_prefix is None:\n        print(\n            \"❌ Please provide either `--conda-env-name` or\"\n            \" `--conda-env-prefix` when using `--conda-lock-file`.\",\n        )\n        sys.exit(1)\n    elif conda_env_name:\n        env_args = [\"--name\", conda_env_name]\n    elif conda_env_prefix:\n        env_args = [\"--prefix\", str(conda_env_prefix)]\n\n    if conda_executable == \"micromamba\":\n        create_cmd = [\n            _maybe_exe(conda_executable),\n            \"create\",\n            \"-f\",\n            str(conda_lock_file),\n            \"--yes\",\n            *env_args,\n        ]\n        if verbose:\n            create_cmd.append(\"--verbose\")\n    else:  # conda or mamba\n        if not dry_run:\n            _verify_conda_lock_installed()\n        create_cmd = [\"conda-lock\", \"install\", *env_args]\n\n        if conda_executable == \"mamba\":\n            create_cmd.append(\"--mamba\")\n        elif conda_executable == \"conda\":\n            create_cmd.extend([\"--conda\", \"conda\"])\n\n        create_cmd.append(str(conda_lock_file))\n\n        if verbose:\n            create_cmd.append(\"--log-level=DEBUG\")\n    create_cmd_str = \" \".join(map(str, create_cmd))\n    env_identifier = (\n        f\"'{conda_env_name}'\" if conda_env_name else f\"at '{conda_env_prefix}'\"\n    )\n    print(f\"📦 Creating conda environment {env_identifier} with `{create_cmd_str}`\")\n\n    if not dry_run:  # pragma: no cover\n        try:\n            subprocess.run(create_cmd, check=True)\n            if verbose:\n                print(f\"✅ Environment {env_identifier} created successfully.\")\n        except subprocess.CalledProcessError as e:\n            print(f\"❌ Failed to create environment: {e}\")\n            sys.exit(1)\n    else:\n        print(\"🏁 Dry run completed. No environment was created.\")\n\n\ndef _verify_conda_lock_installed() -> None:\n    \"\"\"Verify that conda-lock is installed and accessible.\"\"\"\n    if shutil.which(\"conda-lock\") is None:\n        print(\n            \"❌ conda-lock is not installed or not found in PATH.\\n\"\n            \"Please install it with one of the following commands:\\n\"\n            \"  pip install conda-lock\\n\"\n            \"  conda install conda-lock -c conda-forge\\n\"\n            \"  mamba install conda-lock -c conda-forge\",\n        )\n        sys.exit(1)\n\n    try:\n        # Check if conda-lock is working correctly\n        subprocess.run(\n            [\"conda-lock\", \"--version\"],  # noqa: S607\n            check=True,\n            stdout=subprocess.DEVNULL,\n            stderr=subprocess.DEVNULL,\n        )\n    except subprocess.CalledProcessError:\n        print(\n            \"❌ conda-lock is installed but not working correctly.\\n\"\n            \"Please try reinstalling it with one of the following commands:\\n\"\n            \"  `pip install --force-reinstall conda-lock`\\n\"\n            \"  `conda install --force-reinstall conda-lock -c conda-forge`\\n\"\n            \"  `mamba install --force-reinstall conda-lock -c conda-forge`\\n\"\n            \"  `pipx install --force-reinstall conda-lock`\",\n        )\n        sys.exit(1)\n\n    # If we get here, conda-lock is installed and working\n\n\ndef _merge_command(\n    *,\n    depth: int,\n    directory: Path,\n    files: list[Path] | None,\n    name: str,\n    output: Path | None,\n    stdout: bool,\n    selector: Literal[\"sel\", \"comment\"],\n    platforms: list[Platform],\n    ignore_pins: list[str],\n    skip_dependencies: list[str],\n    overwrite_pins: list[str],\n    verbose: bool,\n    optional_dependencies: list[str] | None = None,\n    all_optional_dependencies: bool = False,\n) -> None:  # pragma: no cover\n    # When using stdout, suppress verbose output\n    verbose = verbose and not stdout\n    if output is None:\n        output = Path(\"environment.yaml\")\n\n    if files:  # ignores depth and directory!\n        found_files = files\n    else:\n        found_files = find_requirements_files(\n            directory,\n            depth,\n            verbose=verbose,\n        )\n        if not found_files:\n            print(f\"❌ No {_DEP_FILES} files found in {directory}\")\n            sys.exit(1)\n\n    extras = _merge_optional_dependency_extras(\n        found_files=found_files,\n        optional_dependencies=optional_dependencies or [],\n        all_optional_dependencies=all_optional_dependencies,\n    )\n    requirements = parse_requirements(\n        *found_files,\n        ignore_pins=ignore_pins,\n        overwrite_pins=overwrite_pins,\n        skip_dependencies=skip_dependencies,\n        verbose=verbose,\n        extras=extras,\n    )\n    env_entries = _flatten_selected_dependency_entries(\n        requirements.dependency_entries,\n        requirements.optional_dependency_entries,\n    )\n    platforms = resolve_platforms(\n        requested_platforms=platforms,\n        declared_platforms=requirements.platforms,\n        selector_platforms=_collect_selected_conda_like_platforms(env_entries),\n    )\n    env_spec = create_conda_env_specification(\n        env_entries,\n        requirements.channels,\n        requirements.pip_indices,\n        platforms,\n        selector=selector,\n    )\n    output_file = None if stdout else output\n    write_conda_environment_file(env_spec, output_file, name, verbose=verbose)\n    if output_file:\n        found_files_str = \", \".join(f\"`{f}`\" for f in found_files)\n        print(\n            f\"✅ Generated environment file at `{output_file}` from {found_files_str}\",\n        )\n\n\ndef _pixi_command(\n    *,\n    depth: int,\n    directory: Path,\n    files: list[Path] | None,\n    name: str | None,\n    output: Path | None,\n    stdout: bool,\n    channels: list[str] | None,\n    platforms: list[Platform] | None,\n    ignore_pins: list[str],\n    skip_dependencies: list[str],\n    overwrite_pins: list[str],\n    verbose: bool,\n) -> None:  # pragma: no cover\n    \"\"\"Generate a pixi.toml file from requirements files.\"\"\"\n    # When using stdout, suppress verbose output\n    verbose = verbose and not stdout\n    if output is None:\n        output = Path(\"pixi.toml\")\n\n    if files:  # ignores depth and directory!\n        found_files = files\n    else:\n        found_files = find_requirements_files(\n            directory,\n            depth,\n            verbose=verbose,\n        )\n        if not found_files:\n            print(f\"❌ No {_DEP_FILES} files found in {directory}\")\n            sys.exit(1)\n\n    output_file = None if stdout else output\n    generate_pixi_toml(\n        *found_files,\n        project_name=name,\n        channels=channels,\n        platforms=platforms,\n        output_file=output_file,\n        verbose=verbose,\n        ignore_pins=ignore_pins,\n        skip_dependencies=skip_dependencies,\n        overwrite_pins=overwrite_pins,\n    )\n\n    if output_file:\n        found_files_str = \", \".join(f\"`{f}`\" for f in found_files)\n        print(f\"✅ Generated `{output_file}` from {found_files_str}\")\n        print(\"   Run `pixi install` to install dependencies.\")\n\n\ndef _pip_compile_command(\n    *,\n    depth: int,\n    directory: Path,\n    platform: Platform,\n    ignore_pins: list[str],\n    skip_dependencies: list[str],\n    overwrite_pins: list[str],\n    verbose: bool,\n    extra_flags: list[str],\n    output_file: Path | None = None,\n) -> None:\n    if importlib.util.find_spec(\"piptools\") is None:  # pragma: no cover\n        print(\n            \"❌ Could not import `pip-tools` module.\"\n            \" Please install it with `pip install pip-tools`.\",\n        )\n        sys.exit(1)\n\n    found_files = find_requirements_files(\n        directory,\n        depth,\n        verbose=verbose,\n    )\n\n    requirements = parse_requirements(\n        *found_files,\n        ignore_pins=ignore_pins,\n        overwrite_pins=overwrite_pins,\n        skip_dependencies=skip_dependencies,\n        verbose=verbose,\n    )\n    pip_entries = _flatten_selected_dependency_entries(\n        requirements.dependency_entries,\n        requirements.optional_dependency_entries,\n    )\n    python_deps = filter_python_dependencies(pip_entries, [platform])\n    requirements_in = directory / \"requirements.in\"\n    with requirements_in.open(\"w\") as f:\n        f.write(\"\\n\".join(python_deps))\n    print(\"✅ Generated `requirements.in` file.\")\n    if extra_flags:\n        assert extra_flags[0] == \"--\"\n        extra_flags = extra_flags[1:]\n        if verbose:\n            print(f\"📝 Extra flags for `pip-compile`: {extra_flags}\")\n\n    if output_file is None:\n        output_file = directory / \"requirements.txt\"\n\n    cmd = [\n        \"pip-compile\",\n        \"--output-file\",\n        str(output_file),\n        *extra_flags,\n        str(requirements_in),\n    ]\n    print(f\"🔒 Locking dependencies with `{' '.join(cmd)}`\\n\")\n    subprocess.run(cmd, check=True)\n    if output_file.exists():  # pragma: no cover\n        # might not exist in tests\n        add_comment_to_file(output_file)\n    print(f\"✅ Generated `{output_file}`.\")\n\n\ndef _check_conda_prefix() -> None:  # pragma: no cover\n    \"\"\"Check if sys.executable is in the $CONDA_PREFIX.\"\"\"\n    if \"CONDA_PREFIX\" not in os.environ:\n        return\n    conda_prefix = os.environ[\"CONDA_PREFIX\"]\n    if sys.executable.startswith(str(conda_prefix)):\n        return\n    msg = (\n        \"UniDep should be run from the current Conda environment for correct\"\n        \" operation. However, it's currently running with the Python interpreter\"\n        f\" at `{sys.executable}`, which is not in the active Conda environment\"\n        f\" (`{conda_prefix}`). Please install and run UniDep in the current\"\n        \" Conda environment to avoid any issues, or provide the `--conda-env-name`\"\n        \" or `--conda-env-prefix` option to specify the Conda environment to use.\"\n    )\n    warn(msg, stacklevel=2)\n    sys.exit(1)\n\n\ndef _print_versions() -> None:  # pragma: no cover\n    \"\"\"Print version information.\"\"\"\n    path = Path(__file__).parent\n    txt = [\n        f\"unidep version: {__version__}\",\n        f\"unidep location: {path}\",\n        f\"Python version: {sys.version}\",\n        f\"Python executable: {sys.executable}\",\n    ]\n    extra_packages = [\n        \"rich_argparse\",\n        \"rich\",\n        \"conda_lock\",\n        \"pydantic\",\n        \"pip_tools\",\n        \"conda_package_handling\",\n        \"ruamel.yaml\",\n        \"packaging\",\n        \"tomli\",\n    ]\n    for package in extra_packages:\n        version = get_package_version(package)\n        if version is not None:\n            txt.append(f\"{package} version: {version}\")\n\n    if importlib.util.find_spec(\"rich\") is not None:\n        _print_with_rich(txt)\n    else:\n        print(\"\\n\".join(txt))\n\n\ndef _print_with_rich(data: list) -> None:\n    \"\"\"Print data as a table using rich, if it's installed.\"\"\"\n    from rich.console import Console\n    from rich.table import Table\n\n    console = Console()\n    table = Table(show_header=False)\n    table.add_column(\"Property\", style=\"cyan\")\n    table.add_column(\"Value\", style=\"magenta\")\n    for line in data:\n        prop, value = line.split(\":\", 1)\n        table.add_row(prop, value.strip())\n    console.print(table)\n\n\ndef _pip_subcommand(\n    *,\n    file: list[Path],\n    platforms: list[Platform],\n    verbose: bool,\n    ignore_pins: list[str] | None,\n    skip_dependencies: list[str] | None,\n    overwrite_pins: list[str] | None,\n    separator: str,\n) -> str:  # pragma: no cover\n    platforms = platforms or [identify_current_platform()]\n    assert len(file) <= 1\n    path = file[0] if file else Path()\n    deps = get_python_dependencies(\n        path,\n        platforms=platforms,\n        verbose=verbose,\n        ignore_pins=ignore_pins,\n        skip_dependencies=skip_dependencies,\n        overwrite_pins=overwrite_pins,\n        include_local_dependencies=True,\n    )\n    pip_dependencies = deps.dependencies\n    for extra in parse_folder_or_filename(path).extras:\n        pip_dependencies.extend(deps.extras[extra])\n    return escape_unicode(separator).join(pip_dependencies)\n\n\ndef main() -> None:  # noqa: PLR0912\n    \"\"\"Main entry point for the command-line tool.\"\"\"\n    args = _parse_args()\n\n    if args.command == \"merge\":  # pragma: no cover\n        _merge_command(\n            depth=args.depth,\n            directory=args.directory,\n            files=None,\n            name=args.name,\n            output=args.output,\n            stdout=args.stdout,\n            selector=args.selector,\n            platforms=args.platform,\n            optional_dependencies=args.optional_dependencies,\n            all_optional_dependencies=args.all_optional_dependencies,\n            ignore_pins=args.ignore_pin,\n            skip_dependencies=args.skip_dependency,\n            overwrite_pins=args.overwrite_pin,\n            verbose=args.verbose,\n        )\n    elif args.command == \"pip\":  # pragma: no cover\n        print(\n            _pip_subcommand(\n                file=args.file,\n                platforms=args.platform,\n                verbose=args.verbose,\n                ignore_pins=args.ignore_pin,\n                skip_dependencies=args.skip_dependency,\n                overwrite_pins=args.overwrite_pin,\n                separator=args.separator,\n            ),\n        )\n    elif args.command == \"conda\":  # pragma: no cover\n        platforms = args.platform or [identify_current_platform()]\n        files = args.file or [Path()]\n        requirements = parse_requirements(\n            *files,\n            ignore_pins=args.ignore_pin,\n            skip_dependencies=args.skip_dependency,\n            overwrite_pins=args.overwrite_pin,\n            verbose=args.verbose,\n        )\n        env_entries = _flatten_selected_dependency_entries(\n            requirements.dependency_entries,\n            requirements.optional_dependency_entries,\n        )\n        env_spec = create_conda_env_specification(\n            env_entries,\n            requirements.channels,\n            requirements.pip_indices,\n            platforms=platforms,\n        )\n\n        if any(parse_folder_or_filename(f).extras for f in files):\n            msg = \"🚧 The `conda` command currently does not support extras.\"\n            print(msg)\n            sys.exit(1)\n\n        print(escape_unicode(args.separator).join(env_spec.conda))  # type: ignore[arg-type]\n    elif args.command == \"install\":\n        if args.conda_env_name is None and args.conda_env_prefix is None:\n            _check_conda_prefix()\n        _install_command(\n            *(args.files or [Path()]),\n            conda_executable=args.conda_executable,\n            conda_env_name=args.conda_env_name,\n            conda_env_prefix=args.conda_env_prefix,\n            conda_lock_file=args.conda_lock_file,\n            dry_run=args.dry_run,\n            editable=args.editable,\n            skip_local=args.skip_local,\n            skip_pip=args.skip_pip,\n            skip_conda=args.skip_conda,\n            no_dependencies=args.no_dependencies,\n            ignore_pins=args.ignore_pin,\n            skip_dependencies=args.skip_dependency,\n            overwrite_pins=args.overwrite_pin,\n            no_uv=args.no_uv,\n            verbose=args.verbose,\n        )\n    elif args.command == \"install-all\":\n        if args.conda_env_name is None and args.conda_env_prefix is None:\n            _check_conda_prefix()\n        _install_all_command(\n            conda_executable=args.conda_executable,\n            conda_env_name=args.conda_env_name,\n            conda_env_prefix=args.conda_env_prefix,\n            conda_lock_file=args.conda_lock_file,\n            dry_run=args.dry_run,\n            editable=args.editable,\n            depth=args.depth,\n            directory=args.directory,\n            skip_local=args.skip_local,\n            skip_pip=args.skip_pip,\n            skip_conda=args.skip_conda,\n            no_dependencies=args.no_dependencies,\n            ignore_pins=args.ignore_pin,\n            skip_dependencies=args.skip_dependency,\n            overwrite_pins=args.overwrite_pin,\n            no_uv=args.no_uv,\n            verbose=args.verbose,\n        )\n    elif args.command == \"conda-lock\":  # pragma: no cover\n        conda_lock_command(\n            depth=args.depth,\n            directory=args.directory,\n            files=args.file or None,\n            platforms=args.platform,\n            verbose=args.verbose,\n            only_global=args.only_global,\n            ignore_pins=args.ignore_pin,\n            skip_dependencies=args.skip_dependency,\n            overwrite_pins=args.overwrite_pin,\n            check_input_hash=args.check_input_hash,\n            extra_flags=args.extra_flags,\n            lockfile=args.lockfile,\n        )\n    elif args.command == \"pixi\":  # pragma: no cover\n        _pixi_command(\n            depth=args.depth,\n            directory=args.directory,\n            files=args.file or None,\n            name=args.name,\n            output=args.output,\n            stdout=args.stdout,\n            channels=args.channel or None,\n            platforms=args.platform or None,\n            ignore_pins=args.ignore_pin,\n            skip_dependencies=args.skip_dependency,\n            overwrite_pins=args.overwrite_pin,\n            verbose=args.verbose,\n        )\n    elif args.command == \"pip-compile\":  # pragma: no cover\n        if args.platform and len(args.platform) > 1:\n            print(\n                \"❌ The `pip-compile` command does not support multiple platforms.\",\n            )\n            sys.exit(1)\n        platform = args.platform[0] if args.platform else identify_current_platform()\n        _pip_compile_command(\n            depth=args.depth,\n            directory=args.directory,\n            platform=platform,\n            verbose=args.verbose,\n            ignore_pins=args.ignore_pin,\n            skip_dependencies=args.skip_dependency,\n            overwrite_pins=args.overwrite_pin,\n            extra_flags=args.extra_flags,\n            output_file=args.output_file,\n        )\n    elif args.command == \"version\":  # pragma: no cover\n        _print_versions()\n"
  },
  {
    "path": "unidep/_conda_env.py",
    "content": "\"\"\"unidep - Unified Conda and Pip requirements management.\n\nConda environment file generation functions.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport sys\nfrom collections import defaultdict\nfrom copy import deepcopy\nfrom typing import TYPE_CHECKING, NamedTuple, cast\n\nfrom ruamel.yaml import YAML\nfrom ruamel.yaml.comments import CommentedMap, CommentedSeq\n\nfrom unidep._conflicts import (\n    VersionConflictError,\n    _maybe_new_spec_with_combined_pinnings,\n)\nfrom unidep._dependency_selection import (\n    collapse_selected_universals,\n    select_conda_like_requirements,\n)\nfrom unidep.platform_definitions import (\n    PLATFORM_SELECTOR_MAP,\n    CondaPlatform,\n    Platform,\n    Spec,\n)\nfrom unidep.utils import (\n    add_comment_to_file,\n    build_pep508_environment_marker,\n)\n\nif TYPE_CHECKING:\n    from collections.abc import Sequence\n    from pathlib import Path\n\n    from unidep._dependencies_parsing import DependencyEntry\n\nif sys.version_info >= (3, 8):\n    from typing import Literal, get_args\nelse:  # pragma: no cover\n    from typing_extensions import Literal, get_args\n\n\nclass CondaEnvironmentSpec(NamedTuple):\n    \"\"\"A conda environment.\"\"\"\n\n    channels: list[str]\n    platforms: list[Platform]\n    conda: list[str | dict[str, str]]  # actually a CommentedSeq[str | dict[str, str]]\n    pip: list[str]\n    pip_indices: Sequence[str] = ()\n\n\ndef _conda_sel(sel: str) -> CondaPlatform:\n    \"\"\"Return the allowed `sel(platform)` string.\"\"\"\n    _platform = sel.split(\"-\", 1)[0]\n    assert _platform in get_args(CondaPlatform), f\"Invalid platform: {_platform}\"\n    return cast(\"CondaPlatform\", _platform)\n\n\ndef _as_dependency_entries(\n    entries: Sequence[DependencyEntry],\n) -> list[DependencyEntry]:\n    if isinstance(entries, dict):\n        msg = (\n            \"`create_conda_env_specification()` now requires dependency entries from \"\n            \"`parse_requirements(...).dependency_entries`, not the output of \"\n            \"`resolve_conflicts()`.\"\n        )\n        raise TypeError(msg)\n    return list(entries)\n\n\ndef _normalize_pip_indices(\n    pip_indices: Sequence[str] | None,\n) -> tuple[str, ...]:\n    if pip_indices is None:\n        return ()\n    if isinstance(pip_indices, str):\n        return (pip_indices,)\n    return tuple(pip_indices)\n\n\ndef _extract_conda_pip_dependencies(\n    entries: list[DependencyEntry],\n    platforms: list[Platform],\n) -> tuple[\n    dict[str, dict[Platform | None, Spec]],\n    dict[str, dict[Platform | None, Spec]],\n]:\n    \"\"\"Extract dependencies using the shared conda-like selector.\"\"\"\n    conda: dict[str, dict[Platform | None, Spec]] = {}\n    pip: dict[str, dict[Platform | None, Spec]] = {}\n    selected = collapse_selected_universals(\n        select_conda_like_requirements(entries, platforms),\n        platforms,\n    )\n    for _platform, candidates in selected.items():\n        for candidate in candidates:\n            if candidate.source == \"conda\":\n                conda.setdefault(candidate.spec.name, {})[_platform] = candidate.spec\n            else:\n                pip.setdefault(candidate.spec.name, {})[_platform] = candidate.spec\n    return conda, pip\n\n\ndef _ensure_sel_representable(\n    platform_to_spec: dict[Platform | None, Spec],\n) -> None:\n    \"\"\"Ensure selected specs can be represented with `sel(...)` selectors.\"\"\"\n    grouped: dict[CondaPlatform, list[tuple[Platform, Spec]]] = defaultdict(list)\n    for _platform, spec in sorted(platform_to_spec.items()):\n        assert _platform is not None\n        grouped[_conda_sel(_platform)].append((_platform, spec))\n\n    for conda_platform, platform_specs in grouped.items():\n        keep_platform = platform_specs[0][0]\n        unique_specs = list(dict.fromkeys(spec for _, spec in platform_specs))\n        if len(unique_specs) > 1:\n            try:\n                merged_spec = _maybe_new_spec_with_combined_pinnings(unique_specs)\n            except VersionConflictError:\n                msg = (\n                    \"Selected dependencies cannot be represented with `sel(...)` \"\n                    f\"for '{conda_platform}'. Use selector='comment' instead.\"\n                )\n                raise ValueError(msg) from None\n        else:\n            merged_spec = unique_specs[0]\n\n        for _platform, _spec in platform_specs:\n            if _platform != keep_platform:\n                platform_to_spec.pop(_platform, None)\n        platform_to_spec[keep_platform] = merged_spec\n\n\ndef _add_comment(commment_seq: CommentedSeq, platform: Platform) -> None:\n    comment = f\"# [{PLATFORM_SELECTOR_MAP[platform][0]}]\"\n    commment_seq.yaml_add_eol_comment(comment, len(commment_seq) - 1)\n\n\n_LEGACY_SELECTOR_ARG_COUNT = 2\n_LEGACY_FULL_ARG_COUNT = 3\n\n\ndef create_conda_env_specification(  # noqa: C901, PLR0912, PLR0915\n    entries: Sequence[DependencyEntry],\n    channels: list[str],\n    *args: object,\n    platforms: Sequence[Platform] | None = None,\n    selector: Literal[\"sel\", \"comment\"] = \"sel\",\n    pip_indices: Sequence[str] | None = None,\n) -> CondaEnvironmentSpec:\n    \"\"\"Create a conda environment specification from dependency entries.\n\n    Preferred calling convention:\n    `create_conda_env_specification(entries, channels, platforms, pip_indices=...)`\n\n    For compatibility, the older positional style used during the original\n    `pip_indices` branch development is also accepted:\n    `create_conda_env_specification(entries, channels, pip_indices, platforms)`\n    \"\"\"\n    if platforms is not None:\n        if len(args) > 1:\n            msg = (\n                \"Too many positional arguments for `create_conda_env_specification()`.\"\n            )\n            raise TypeError(msg)\n        if args:\n            if pip_indices is not None:\n                msg = \"`pip_indices` was provided both positionally and by keyword.\"\n                raise TypeError(msg)\n            pip_indices = cast(\"Sequence[str]\", args[0])\n        resolved_platforms = list(platforms)\n    else:\n        if not args:\n            msg = \"Missing required `platforms` argument.\"\n            raise TypeError(msg)\n        if len(args) == 1:\n            resolved_platforms = list(cast(\"Sequence[Platform]\", args[0]))\n        elif len(args) == _LEGACY_SELECTOR_ARG_COUNT:\n            if args[1] in (\"sel\", \"comment\"):\n                resolved_platforms = list(cast(\"Sequence[Platform]\", args[0]))\n                selector = cast(\"Literal['sel', 'comment']\", args[1])\n            else:\n                if pip_indices is not None:\n                    msg = \"`pip_indices` was provided both positionally and by keyword.\"\n                    raise TypeError(msg)\n                pip_indices = cast(\"Sequence[str]\", args[0])\n                resolved_platforms = list(cast(\"Sequence[Platform]\", args[1]))\n        elif len(args) == _LEGACY_FULL_ARG_COUNT:\n            if pip_indices is not None:\n                msg = \"`pip_indices` was provided both positionally and by keyword.\"\n                raise TypeError(msg)\n            pip_indices = cast(\"Sequence[str]\", args[0])\n            resolved_platforms = list(cast(\"Sequence[Platform]\", args[1]))\n            selector = cast(\"Literal['sel', 'comment']\", args[2])\n        else:\n            msg = (\n                \"Too many positional arguments for `create_conda_env_specification()`.\"\n            )\n            raise TypeError(msg)\n\n    if selector not in (\"sel\", \"comment\"):  # pragma: no cover\n        msg = f\"Invalid selector: {selector}, must be one of ['sel', 'comment']\"\n        raise ValueError(msg)\n\n    entries = _as_dependency_entries(entries)\n    conda, pip = _extract_conda_pip_dependencies(entries, resolved_platforms)\n    normalized_pip_indices = _normalize_pip_indices(pip_indices)\n\n    conda_deps: list[str | dict[str, str]] = CommentedSeq()\n    pip_deps: list[str] = CommentedSeq()\n    for platform_to_spec in conda.values():\n        if len(platform_to_spec) > 1 and selector == \"sel\":\n            _ensure_sel_representable(platform_to_spec)\n        for _platform, spec in sorted(platform_to_spec.items()):\n            dep_str = spec.name_with_pin()\n            if len(resolved_platforms) != 1 and _platform is not None:\n                if selector == \"sel\":\n                    sel = _conda_sel(_platform)\n                    dep_str = {f\"sel({sel})\": dep_str}  # type: ignore[assignment]\n                conda_deps.append(dep_str)\n                if selector == \"comment\":\n                    _add_comment(conda_deps, _platform)\n            else:\n                conda_deps.append(dep_str)\n\n    for platform_to_spec in pip.values():\n        spec_to_platforms: dict[Spec, list[Platform | None]] = {}\n        for _platform, spec in platform_to_spec.items():\n            spec_to_platforms.setdefault(spec, []).append(_platform)\n\n        for spec, _platforms in spec_to_platforms.items():\n            dep_str = spec.name_with_pin(is_pip=True)\n            if _platforms != [None] and len(resolved_platforms) != 1:\n                if selector == \"sel\":\n                    marker = build_pep508_environment_marker(_platforms)  # type: ignore[arg-type]\n                    dep_str = f\"{dep_str}; {marker}\"\n                    pip_deps.append(dep_str)\n                else:\n                    assert selector == \"comment\"\n                    # We can only add comments with a single platform because\n                    # `conda-lock` doesn't implement logic, e.g., [linux or win]\n                    # should be spread into two lines, one with [linux] and the\n                    # other with [win].\n                    for _platform in _platforms:\n                        pip_deps.append(dep_str)\n                        _add_comment(pip_deps, cast(\"Platform\", _platform))\n            else:\n                pip_deps.append(dep_str)\n\n    return CondaEnvironmentSpec(\n        channels,\n        resolved_platforms,\n        conda_deps,\n        pip_deps,\n        normalized_pip_indices,\n    )\n\n\ndef write_conda_environment_file(\n    env_spec: CondaEnvironmentSpec,\n    output_file: str | Path | None = \"environment.yaml\",\n    name: str = \"myenv\",\n    *,\n    verbose: bool = False,\n) -> None:\n    \"\"\"Generate a conda environment.yaml file or print to stdout.\"\"\"\n    resolved_dependencies = deepcopy(env_spec.conda)\n    if env_spec.pip:\n        resolved_dependencies.append({\"pip\": env_spec.pip})  # type: ignore[arg-type, dict-item]\n    env_data = CommentedMap({\"name\": name})\n    if env_spec.channels:\n        env_data[\"channels\"] = env_spec.channels\n    if env_spec.pip_indices:\n        env_data[\"pip-repositories\"] = list(env_spec.pip_indices)\n    if resolved_dependencies:\n        env_data[\"dependencies\"] = resolved_dependencies\n    if env_spec.platforms:\n        env_data[\"platforms\"] = env_spec.platforms\n    yaml = YAML(typ=\"rt\")\n    yaml.default_flow_style = False\n    yaml.width = 4096\n    yaml.indent(mapping=2, sequence=2, offset=2)\n    if output_file:\n        if verbose:\n            print(f\"📝 Generating environment file at `{output_file}`\")\n        with open(output_file, \"w\") as f:  # noqa: PTH123\n            yaml.dump(env_data, f)\n        if verbose:\n            print(\"📝 Environment file generated successfully.\")\n        add_comment_to_file(output_file)\n    else:\n        yaml.dump(env_data, sys.stdout)\n"
  },
  {
    "path": "unidep/_conda_lock.py",
    "content": "\"\"\"unidep - Unified Conda and Pip requirements management.\n\nThis module provides the `unidep conda-lock` CLI command, used in `unidep._cli`.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport shutil\nimport subprocess\nimport sys\nimport tempfile\nimport urllib.request\nfrom collections import defaultdict\nfrom functools import partial\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, NamedTuple\n\nfrom packaging.utils import canonicalize_name\nfrom ruamel.yaml import YAML\n\nfrom unidep._dependencies_parsing import find_requirements_files, parse_requirements\nfrom unidep._dependency_selection import (\n    collapse_selected_universals,\n    select_conda_like_requirements,\n)\nfrom unidep.utils import (\n    add_comment_to_file,\n    remove_top_comments,\n    warn,\n)\n\nif TYPE_CHECKING:\n    from unidep.platform_definitions import CondaPip, Platform\n\n    if sys.version_info >= (3, 8):\n        from typing import Literal\n    else:  # pragma: no cover\n        from typing_extensions import Literal\n\n\ndef _run_conda_lock(\n    tmp_env: Path,\n    conda_lock_output: Path,\n    *,\n    check_input_hash: bool = False,\n    extra_flags: list[str],\n) -> None:  # pragma: no cover\n    if shutil.which(\"conda-lock\") is None:\n        msg = (\n            \"Cannot find `conda-lock`.\"\n            \" Please install it with `pip install conda-lock`, or\"\n            \" `pipx install conda-lock`, or\"\n            \" `conda install -c conda-forge conda-lock`.\"\n        )\n        raise RuntimeError(msg)\n    if not check_input_hash and conda_lock_output.exists():\n        print(f\"🗑️ Removing existing `{conda_lock_output}`\")\n        conda_lock_output.unlink()\n    cmd = [\n        \"conda-lock\",\n        \"lock\",\n        \"--file\",\n        str(tmp_env),\n        \"--lockfile\",\n        str(conda_lock_output),\n        *extra_flags,\n    ]\n    if check_input_hash:\n        cmd.append(\"--check-input-hash\")\n    print(f\"🔒 Locking dependencies with `{' '.join(cmd)}`\\n\")\n    try:\n        subprocess.run(cmd, check=True, text=True, capture_output=True)\n        remove_top_comments(conda_lock_output)\n        add_comment_to_file(\n            conda_lock_output,\n            extra_lines=[\n                \"#\",\n                \"# This environment can be installed with\",\n                \"# `micromamba create -f conda-lock.yml -n myenv`\",\n                \"# This file is a `conda-lock` file generated via `unidep`.\",\n                \"# For details see https://conda.github.io/conda-lock/\",\n            ],\n        )\n    except subprocess.CalledProcessError as e:\n        print(\"❌ Error occurred:\\n\", e)\n        print(\"Return code:\", e.returncode)\n        print(\"Output:\", e.output)\n        print(\"Error Output:\", e.stderr)\n        sys.exit(1)\n\n\ndef _conda_lock_global(\n    *,\n    depth: int,\n    directory: Path,\n    files: list[Path] | None,\n    platforms: list[Platform],\n    verbose: bool,\n    check_input_hash: bool,\n    ignore_pins: list[str],\n    skip_dependencies: list[str],\n    overwrite_pins: list[str],\n    extra_flags: list[str],\n    lockfile: str,\n) -> Path:\n    \"\"\"Generate a conda-lock file for the global dependencies.\"\"\"\n    from unidep._cli import _merge_command\n\n    if files:\n        directory = files[0].parent\n\n    tmp_env = directory / \"tmp.environment.yaml\"\n    conda_lock_output = directory / lockfile\n    _merge_command(\n        depth=depth,\n        directory=directory,\n        files=files,\n        name=\"myenv\",\n        output=tmp_env,\n        stdout=False,\n        selector=\"comment\",\n        platforms=platforms,\n        optional_dependencies=[],\n        all_optional_dependencies=False,\n        ignore_pins=ignore_pins,\n        overwrite_pins=overwrite_pins,\n        skip_dependencies=skip_dependencies,\n        verbose=verbose,\n    )\n    _run_conda_lock(\n        tmp_env,\n        conda_lock_output,\n        check_input_hash=check_input_hash,\n        extra_flags=extra_flags,\n    )\n    print(f\"✅ Global dependencies locked successfully in `{conda_lock_output}`.\")\n    return conda_lock_output\n\n\nclass LockSpec(NamedTuple):\n    \"\"\"A specification of the lock file.\"\"\"\n\n    packages: dict[tuple[CondaPip, Platform, str], dict[str, Any]]\n    dependencies: dict[tuple[CondaPip, Platform, str], set[str]]\n\n\ndef _parse_conda_lock_packages(\n    conda_lock_packages: list[dict[str, Any]],\n) -> LockSpec:\n    deps: dict[CondaPip, dict[Platform, dict[str, set[str]]]] = defaultdict(\n        lambda: defaultdict(lambda: defaultdict(set)),\n    )\n\n    def _recurse(\n        package_name: str,\n        resolved: dict[str, set[str]],\n        dependencies: dict[str, set[str]],\n        seen: set[str],\n    ) -> set[str]:\n        if package_name in resolved:\n            return resolved[package_name]\n        if package_name in seen:  # Circular dependency detected\n            return set()\n        seen.add(package_name)\n\n        all_deps = set(dependencies[package_name])\n        for dep in dependencies[package_name]:\n            all_deps.update(_recurse(dep, resolved, dependencies, seen))\n\n        resolved[package_name] = all_deps\n        seen.remove(package_name)\n        return all_deps\n\n    for p in conda_lock_packages:\n        deps[p[\"manager\"]][p[\"platform\"]][p[\"name\"]].update(p[\"dependencies\"])\n\n    resolved: dict[CondaPip, dict[Platform, dict[str, set[str]]]] = {}\n    for manager, platforms in deps.items():\n        resolved_manager = resolved.setdefault(manager, {})\n        for _platform, pkgs in platforms.items():\n            _resolved: dict[str, set[str]] = {}\n            for package in list(pkgs):\n                _recurse(package, _resolved, pkgs, set())\n            resolved_manager[_platform] = _resolved\n\n    packages: dict[tuple[CondaPip, Platform, str], dict[str, Any]] = {}\n    for p in conda_lock_packages:\n        key = (p[\"manager\"], p[\"platform\"], p[\"name\"])\n        assert key not in packages\n        packages[key] = p\n\n    # Flatten the `dependencies` dict to same format as `packages`\n    dependencies = {\n        (which, platform, name): deps\n        for which, platforms in resolved.items()\n        for platform, pkgs in platforms.items()\n        for name, deps in pkgs.items()\n    }\n    return LockSpec(packages, dependencies)\n\n\ndef _add_package_to_lock(\n    *,\n    name: str,\n    which: CondaPip,\n    platform: Platform,\n    packages: dict[tuple[CondaPip, Platform, str], dict[str, Any]],\n    locked: list[dict[str, Any]],\n    locked_keys: set[tuple[CondaPip, Platform, str]],\n) -> tuple[CondaPip, Platform, str] | None:\n    key = _find_lock_key(\n        name=name,\n        which=which,\n        platform=platform,\n        packages=packages,\n    )\n    if key is None:\n        return None\n    if key not in locked_keys:\n        locked.append(packages[key])\n        locked_keys.add(key)  # Add identifier to the set\n    return key\n\n\ndef _strip_pip_extras(name: str) -> str:\n    if not name.endswith(\"]\") or \"[\" not in name:\n        return name\n    return name.split(\"[\", 1)[0]\n\n\ndef _find_lock_key(\n    *,\n    name: str,\n    which: CondaPip,\n    platform: Platform,\n    packages: dict[tuple[CondaPip, Platform, str], dict[str, Any]],\n) -> tuple[CondaPip, Platform, str] | None:\n    key = (which, platform, name)\n    if key in packages:\n        return key\n    if which != \"pip\":\n        return None\n    normalized_name = canonicalize_name(_strip_pip_extras(name))\n    for _which, _platform, _name in packages:\n        if _which != which or _platform != platform:\n            continue\n        if canonicalize_name(_strip_pip_extras(_name)) == normalized_name:\n            return (_which, _platform, _name)\n    return None\n\n\ndef _add_package_with_dependencies_to_lock(\n    *,\n    name: str,\n    which: CondaPip,\n    platform: Platform,\n    lock_spec: LockSpec,\n    locked: list[dict[str, Any]],\n    locked_keys: set[tuple[CondaPip, Platform, str]],\n    missing_keys: set[tuple[CondaPip, Platform, str]],\n) -> None:\n    found_key = _find_lock_key(\n        name=name,\n        which=which,\n        platform=platform,\n        packages=lock_spec.packages,\n    )\n    if found_key is None:\n        missing_keys.add((which, platform, name))\n        return\n    _add_package_to_lock(\n        name=found_key[2],\n        which=found_key[0],\n        platform=found_key[1],\n        packages=lock_spec.packages,\n        locked=locked,\n        locked_keys=locked_keys,\n    )\n    for dep in lock_spec.dependencies.get(found_key, set()):\n        if dep.startswith(\"__\"):  # pragma: no cover\n            continue  # Skip meta packages\n        dep_key = _add_package_to_lock(\n            name=dep,\n            which=which,\n            platform=platform,\n            packages=lock_spec.packages,\n            locked=locked,\n            locked_keys=locked_keys,\n        )\n        if dep_key is None:\n            missing_keys.add((which, platform, dep))\n\n\ndef _handle_missing_keys(\n    lock_spec: LockSpec,\n    locked_keys: set[tuple[CondaPip, Platform, str]],\n    missing_keys: set[tuple[CondaPip, Platform, str]],\n    locked: list[dict[str, Any]],\n) -> None:\n    add_pkg = partial(\n        _add_package_with_dependencies_to_lock,\n        lock_spec=lock_spec,\n        locked=locked,\n        locked_keys=locked_keys,\n        missing_keys=missing_keys,\n    )\n\n    # Do not re-add packages that with pip that are\n    # already added with conda\n    for which, _platform, name in locked_keys:\n        if which == \"conda\":\n            key = (\"pip\", _platform, name)\n            missing_keys.discard(key)  # type: ignore[arg-type]\n\n    # Add missing pip packages using conda (if possible)\n    for which, _platform, name in list(missing_keys):\n        if which == \"pip\":\n            missing_keys.discard((which, _platform, name))\n            add_pkg(name=name, which=\"conda\", platform=_platform)\n            if (\"conda\", _platform, name) in missing_keys:\n                # If the package wasn't added, restore the missing key\n                missing_keys.discard((\"conda\", _platform, name))\n                missing_keys.add((\"pip\", _platform, name))\n\n    if not missing_keys:\n        return\n\n    # Finally there might be some pip packages that are missing\n    # because in the lock file they are installed with conda, however,\n    # on Conda the name might be different than on PyPI. For example,\n    # `msgpack` (pip) and `msgpack-python` (conda).\n    options = {\n        (which, platform, name): pkg\n        for which, platform, name in missing_keys\n        for (_which, _platform, _name), pkg in lock_spec.packages.items()\n        if which == \"pip\"\n        and _which == \"conda\"\n        and platform == _platform\n        and name in _name\n    }\n    for (which, _platform, name), pkg in options.items():\n        names = _download_and_get_package_names(pkg)\n        if names is None:\n            continue\n        if name in names:\n            add_pkg(name=pkg[\"name\"], which=pkg[\"manager\"], platform=pkg[\"platform\"])\n            missing_keys.discard((which, _platform, name))\n    if missing_keys:\n        print(f\"❌ Missing keys {missing_keys}\")\n\n\ndef _conda_lock_subpackage(\n    *,\n    file: Path,\n    lock_spec: LockSpec,\n    channels: list[str],\n    platforms: list[Platform],\n    yaml: YAML | None,  # Passing this to preserve order!\n) -> Path:\n    requirements = parse_requirements(file)\n    locked: list[dict[str, Any]] = []\n    locked_keys: set[tuple[CondaPip, Platform, str]] = set()\n    missing_keys: set[tuple[CondaPip, Platform, str]] = set()\n\n    add_pkg = partial(\n        _add_package_with_dependencies_to_lock,\n        lock_spec=lock_spec,\n        locked=locked,\n        locked_keys=locked_keys,\n        missing_keys=missing_keys,\n    )\n\n    selected = collapse_selected_universals(\n        select_conda_like_requirements(requirements.dependency_entries, platforms),\n        platforms,\n    )\n    for target_platform, candidates in selected.items():\n        candidate_platforms = (\n            platforms if target_platform is None else [target_platform]\n        )\n        for candidate in candidates:\n            if candidate.spec.name.startswith(\"__\"):  # pragma: no cover\n                continue\n            for candidate_platform in candidate_platforms:\n                add_pkg(\n                    name=candidate.spec.name,\n                    which=candidate.source,\n                    platform=candidate_platform,\n                )\n    _handle_missing_keys(\n        lock_spec=lock_spec,\n        locked_keys=locked_keys,\n        missing_keys=missing_keys,\n        locked=locked,\n    )\n\n    # Sort locked packages by manager, name, platform\n    locked = sorted(locked, key=lambda p: (p[\"manager\"], p[\"name\"], p[\"platform\"]))\n\n    # Sort dependencies within each package\n    for package in locked:\n        deps = package[\"dependencies\"]\n        if deps:\n            package[\"dependencies\"] = dict(sorted(deps.items()))\n\n    if yaml is None:  # pragma: no cover\n        # When passing the same YAML instance that is used to load the file,\n        # we preserve the order of the keys.\n        yaml = YAML(typ=\"rt\")\n    yaml.default_flow_style = False\n    yaml.width = 4096\n    yaml.representer.ignore_aliases = lambda *_: True  # Disable anchors\n    conda_lock_output = file.parent / \"conda-lock.yml\"\n    metadata = {\n        \"content_hash\": {p: \"unidep-is-awesome\" for p in platforms},\n        \"channels\": [{\"url\": c, \"used_env_vars\": []} for c in channels],\n        \"platforms\": platforms,\n        \"sources\": [str(file)],\n    }\n    with conda_lock_output.open(\"w\") as fp:\n        yaml.dump({\"version\": 1, \"metadata\": metadata, \"package\": locked}, fp)\n    add_comment_to_file(\n        conda_lock_output,\n        extra_lines=[\n            \"#\",\n            \"# This environment can be installed with\",\n            \"# `micromamba create -f conda-lock.yml -n myenv`\",\n            \"# This file is a `conda-lock` file generated via `unidep`.\",\n            \"# For details see https://conda.github.io/conda-lock/\",\n        ],\n    )\n    return conda_lock_output\n\n\ndef _download_and_get_package_names(\n    package: dict[str, Any],\n    component: Literal[\"info\", \"pkg\"] | None = None,\n) -> list[str] | None:\n    try:\n        import conda_package_handling.api\n    except ImportError:  # pragma: no cover\n        print(\n            \"❌ Could not import `conda-package-handling` module.\"\n            \" Please install it with `pip install conda-package-handling`.\",\n        )\n        sys.exit(1)\n    url = package[\"url\"]\n    with tempfile.TemporaryDirectory() as temp_dir:\n        temp_path = Path(temp_dir)\n        file_path = temp_path / Path(url).name\n        urllib.request.urlretrieve(url, str(file_path))  # noqa: S310\n        conda_package_handling.api.extract(\n            str(file_path),\n            dest_dir=str(temp_path),\n            components=component,\n        )\n\n        if (temp_path / \"site-packages\").exists():\n            site_packages_path = temp_path / \"site-packages\"\n        elif (temp_path / \"lib\").exists():\n            lib_path = temp_path / \"lib\"\n            python_dirs = [\n                d\n                for d in lib_path.iterdir()\n                if d.is_dir() and d.name.startswith(\"python\")\n            ]\n            if not python_dirs:\n                return None\n            site_packages_path = python_dirs[0] / \"site-packages\"\n        else:\n            return None\n\n        if not site_packages_path.exists():\n            return None\n\n        return [\n            d.name\n            for d in site_packages_path.iterdir()\n            if d.is_dir() and not d.name.endswith((\".dist-info\", \".egg-info\"))\n        ]\n\n\ndef _conda_lock_subpackages(\n    directory: Path,\n    depth: int,\n    conda_lock_file: str | Path,\n) -> list[Path]:\n    conda_lock_file = Path(conda_lock_file)\n    with YAML(typ=\"rt\") as yaml, conda_lock_file.open() as fp:\n        data = yaml.load(fp)\n    channels = [c[\"url\"] for c in data[\"metadata\"][\"channels\"]]\n    platforms = data[\"metadata\"][\"platforms\"]\n    lock_spec = _parse_conda_lock_packages(data[\"package\"])\n\n    lock_files: list[Path] = []\n    # Assumes that different platforms have the same versions\n    found_files = find_requirements_files(directory, depth)\n    for file in found_files:\n        if file.parent == directory:\n            # This is a `requirements.yaml` file in the root directory\n            # for e.g., common packages, so skip it.\n            continue\n        sublock_file = _conda_lock_subpackage(\n            file=file,\n            lock_spec=lock_spec,\n            channels=channels,\n            platforms=platforms,\n            yaml=yaml,\n        )\n        print(f\"📝 Generated lock file for `{file}`: `{sublock_file}`\")\n        lock_files.append(sublock_file)\n    return lock_files\n\n\ndef conda_lock_command(\n    *,\n    depth: int,\n    directory: Path,\n    files: list[Path] | None,\n    platforms: list[Platform],\n    verbose: bool,\n    only_global: bool,\n    check_input_hash: bool,\n    ignore_pins: list[str],\n    skip_dependencies: list[str],\n    overwrite_pins: list[str],\n    extra_flags: list[str],\n    lockfile: str = \"conda-lock.yml\",\n) -> None:\n    \"\"\"Generate a conda-lock file a collection of `requirements.yaml` and/or `pyproject.toml` files.\"\"\"  # noqa: E501\n    if extra_flags:\n        assert extra_flags[0] == \"--\"\n        extra_flags = extra_flags[1:]\n        if verbose:\n            print(f\"📝 Extra flags for `conda-lock lock`: {extra_flags}\")\n\n    conda_lock_output = _conda_lock_global(\n        depth=depth,\n        directory=directory,\n        files=files,\n        platforms=platforms,\n        verbose=verbose,\n        check_input_hash=check_input_hash,\n        ignore_pins=ignore_pins,\n        overwrite_pins=overwrite_pins,\n        skip_dependencies=skip_dependencies,\n        extra_flags=extra_flags,\n        lockfile=lockfile,\n    )\n    if only_global or files:\n        return\n    sub_lock_files = _conda_lock_subpackages(\n        directory=directory,\n        depth=depth,\n        conda_lock_file=conda_lock_output,\n    )\n    mismatches = _check_consistent_lock_files(\n        global_lock_file=conda_lock_output,\n        sub_lock_files=sub_lock_files,\n    )\n    if not mismatches:\n        print(\"✅ Analyzed all lock files and found no inconsistencies.\")\n    elif len(mismatches) > 1:  # pragma: no cover\n        print(\"❌ Complete table of package version mismatches:\")\n        _mismatch_report(mismatches, raises=False)\n\n\nclass Mismatch(NamedTuple):\n    \"\"\"A mismatch between a global and subpackage lock file.\"\"\"\n\n    name: str\n    version: str\n    version_global: str\n    platform: Platform\n    lock_file: Path\n    which: CondaPip\n\n\ndef _check_consistent_lock_files(\n    global_lock_file: Path,\n    sub_lock_files: list[Path],\n) -> list[Mismatch]:\n    yaml = YAML(typ=\"safe\")\n    with global_lock_file.open() as fp:\n        global_data = yaml.load(fp)\n\n    global_packages: dict[str, dict[Platform, dict[CondaPip, str]]] = defaultdict(\n        lambda: defaultdict(dict),\n    )\n    for p in global_data[\"package\"]:\n        global_packages[p[\"name\"]][p[\"platform\"]][p[\"manager\"]] = p[\"version\"]\n\n    mismatched_packages = []\n    for lock_file in sub_lock_files:\n        with lock_file.open() as fp:\n            data = yaml.load(fp)\n\n        for p in data[\"package\"]:\n            name = p[\"name\"]\n            platform = p[\"platform\"]\n            version = p[\"version\"]\n            which = p[\"manager\"]\n            if global_packages.get(name, {}).get(platform, {}).get(which) == version:\n                continue\n\n            global_version = global_packages[name][platform][which]\n            if global_version != version:\n                mismatched_packages.append(\n                    Mismatch(\n                        name=name,\n                        version=version,\n                        version_global=global_version,\n                        platform=platform,\n                        lock_file=lock_file,\n                        which=which,\n                    ),\n                )\n    return mismatched_packages\n\n\ndef _format_table_row(\n    row: list[str],\n    widths: list[int],\n    seperator: str = \" | \",\n) -> str:  # pragma: no cover\n    \"\"\"Format a row of the table with specified column widths.\"\"\"\n    return seperator.join(f\"{cell:<{widths[i]}}\" for i, cell in enumerate(row))\n\n\ndef _mismatch_report(\n    mismatched_packages: list[Mismatch],\n    *,\n    raises: bool = False,\n) -> None:  # pragma: no cover\n    if not mismatched_packages:\n        return\n\n    headers = [\n        \"Subpackage\",\n        \"Manager\",\n        \"Package\",\n        \"Version (Sub)\",\n        \"Version (Global)\",\n        \"Platform\",\n    ]\n\n    def _to_seq(m: Mismatch) -> list[str]:\n        return [\n            m.lock_file.parent.name,\n            m.which,\n            m.name,\n            m.version,\n            m.version_global,\n            str(m.platform),\n        ]\n\n    column_widths = [len(header) for header in headers]\n    for m in mismatched_packages:\n        attrs = _to_seq(m)\n        for i, attr in enumerate(attrs):\n            column_widths[i] = max(column_widths[i], len(attr))\n\n    # Create the table rows\n    separator_line = [w * \"-\" for w in column_widths]\n    table_rows = [\n        _format_table_row(separator_line, column_widths, seperator=\"-+-\"),\n        _format_table_row(headers, column_widths),\n        _format_table_row([\"-\" * width for width in column_widths], column_widths),\n    ]\n    for m in mismatched_packages:\n        row = _to_seq(m)\n        table_rows.append(_format_table_row(row, column_widths))\n    table_rows.append(_format_table_row(separator_line, column_widths, seperator=\"-+-\"))\n\n    table = \"\\n\".join(table_rows)\n\n    full_error_message = (\n        \"Version mismatches found between global and subpackage lock files:\\n\"\n        + table\n        + \"\\n\\n‼️ You might want to pin some versions stricter\"\n        \" in your `requirements.yaml` and/or `pyproject.toml` files.\"\n    )\n\n    if raises:\n        raise RuntimeError(full_error_message)\n    warn(full_error_message, stacklevel=2)\n"
  },
  {
    "path": "unidep/_conflicts.py",
    "content": "\"\"\"unidep - Unified Conda and Pip requirements management.\n\nVerion conflict detections and resolution.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport sys\nfrom collections import defaultdict\nfrom typing import TYPE_CHECKING\n\nfrom packaging import version\n\nfrom unidep.platform_definitions import Platform, Spec\nfrom unidep.utils import defaultdict_to_dict\n\nif sys.version_info >= (3, 8):\n    from typing import get_args\nelse:  # pragma: no cover\n    from typing_extensions import get_args\n\n\nif TYPE_CHECKING:\n    from unidep.platform_definitions import CondaPip\n\n# Full PEP 440 + conda operator set, ordered longest-prefix-first for matching\nALL_VERSION_OPERATORS: tuple[str, ...] = (\n    \"===\",\n    \"==\",\n    \"~=\",\n    \">=\",\n    \"<=\",\n    \"!=\",\n    \">\",\n    \"<\",\n    \"=\",\n)\n\n# Subset used for conflict resolution (excludes PEP 440-only operators)\nVALID_OPERATORS = [op for op in ALL_VERSION_OPERATORS if op not in (\"===\", \"==\", \"~=\")]\n\n_REPO_URL = \"https://github.com/basnijholt/unidep\"\n\n\ndef extract_version_operator(constraint: str) -> str:\n    \"\"\"Extract the version operator prefix from a constraint string.\n\n    Returns the matched operator or \"\" if none matches.\n    This is a pure extraction helper — it does not validate.\n    \"\"\"\n    constraint = constraint.strip()\n    return next(\n        (op for op in ALL_VERSION_OPERATORS if constraint.startswith(op)),\n        \"\",\n    )\n\n\ndef _prepare_specs_for_conflict_resolution(\n    requirements: dict[str, list[Spec]],\n) -> dict[str, dict[Platform | None, dict[CondaPip, list[Spec]]]]:\n    \"\"\"Prepare and group metadata for conflict resolution.\n\n    This function groups metadata by platform and source for each package.\n\n    :param requirements: Dictionary mapping package names to a list of Spec objects.\n    :return: Dictionary mapping package names to grouped metadata.\n    \"\"\"\n    prepared_data = {}\n    for package, spec_list in requirements.items():\n        grouped_specs: dict[Platform | None, dict[CondaPip, list[Spec]]] = defaultdict(\n            lambda: defaultdict(list),\n        )\n        for spec in spec_list:\n            _platforms = spec.platforms()\n            if _platforms is None:\n                _platforms = [None]  # type: ignore[list-item]\n            for _platform in _platforms:\n                grouped_specs[_platform][spec.which].append(spec)\n\n        prepared_data[package] = grouped_specs\n    return defaultdict_to_dict(prepared_data)\n\n\ndef _pop_unused_platforms_and_maybe_expand_none(\n    platform_data: dict[Platform | None, dict[CondaPip, list[Spec]]],\n    platforms: list[Platform] | None,\n) -> None:\n    \"\"\"Expand `None` to all platforms if there is a platform besides None.\"\"\"\n    allowed_platforms = get_args(Platform)\n    if platforms:\n        allowed_platforms = platforms  # type: ignore[assignment]\n\n    # If there is a platform besides None, expand None to all platforms\n    if len(platform_data) > 1 and None in platform_data:\n        sources = platform_data.pop(None)\n        for _platform in allowed_platforms:\n            for which, specs in sources.items():\n                platform_data.setdefault(_platform, {}).setdefault(which, []).extend(\n                    specs,\n                )\n\n    # Remove platforms that are not allowed\n    to_pop = platform_data.keys() - allowed_platforms\n    to_pop.discard(None)\n    for _platform in to_pop:\n        platform_data.pop(_platform)\n\n\ndef _maybe_new_spec_with_combined_pinnings(\n    specs: list[Spec],\n) -> Spec:\n    pinned_specs = [m for m in specs if m.pin is not None]\n    if len(pinned_specs) == 1:\n        return pinned_specs[0]\n    if len(pinned_specs) > 1:\n        first = pinned_specs[0]\n        pins = [m.pin for m in pinned_specs]\n        pin = combine_version_pinnings(pins, name=first.name)  # type: ignore[arg-type]\n        return Spec(\n            name=first.name,\n            which=first.which,\n            pin=pin,\n            identifier=first.identifier,  # should I create a new one?\n        )\n\n    # Flatten the list\n    return specs[0]\n\n\ndef _combine_pinning_within_platform(\n    data: dict[Platform | None, dict[CondaPip, list[Spec]]],\n) -> dict[Platform | None, dict[CondaPip, Spec]]:\n    reduced_data: dict[Platform | None, dict[CondaPip, Spec]] = {}\n    for _platform, packages in data.items():\n        reduced_data[_platform] = {}\n        for which, specs in packages.items():\n            spec = _maybe_new_spec_with_combined_pinnings(specs)\n            reduced_data[_platform][which] = spec\n    return reduced_data\n\n\nclass VersionConflictError(ValueError):\n    \"\"\"Raised when a version conflict is detected.\"\"\"\n\n\ndef _add_optional_dependencies(\n    requirements: dict[str, list[Spec]],\n    optional_dependencies: dict[str, dict[str, list[Spec]]] | None,\n) -> None:\n    \"\"\"Add optional dependencies to the requirements dictionary.\"\"\"\n    if optional_dependencies is None:\n        return\n    for dependencies in optional_dependencies.values():\n        for pkg, specs in dependencies.items():\n            requirements.setdefault(pkg, []).extend(specs)\n\n\ndef resolve_conflicts(\n    requirements: dict[str, list[Spec]],\n    platforms: list[Platform] | None = None,\n    optional_dependencies: dict[str, dict[str, list[Spec]]] | None = None,\n) -> dict[str, dict[Platform | None, dict[CondaPip, Spec]]]:\n    \"\"\"Resolve conflicts in a dict-based requirements model.\n\n    This helper consolidates within-source duplicates on\n    ``ParsedRequirements.requirements`` and preserves conda/pip alternatives in the\n    returned metadata. CLI-facing renderers instead consume\n    ``parse_requirements(...).dependency_entries`` and apply source selection later.\n\n    Parameters\n    ----------\n    requirements\n        Dictionary mapping package names to a list of Spec objects.\n        Typically ``ParsedRequirements.requirements`` is passed here, which is\n        returned by `parse_requirements`.\n    platforms\n        List of platforms to resolve conflicts for.\n        Typically ``ParsedRequirements.platforms`` is passed here, which is\n        returned by `parse_requirements`.\n    optional_dependencies\n        Dictionary mapping package names to a dictionary of optional dependencies.\n        Typically ``ParsedRequirements.optional_dependencies`` is passed here, which is\n        returned by `parse_requirements`. If passing this argument, all optional\n        dependencies will be added to the requirements dictionary. Pass `None` to\n        ignore optional dependencies.\n\n    Returns\n    -------\n    Dictionary mapping package names to a dictionary of resolved metadata.\n    The resolved metadata is a dictionary mapping platforms to a dictionary\n    mapping sources to a single `Spec` object.\n\n    \"\"\"\n    if platforms and not set(platforms).issubset(get_args(Platform)):\n        msg = f\"Invalid platform: {platforms}, must contain only {get_args(Platform)}\"\n        raise VersionConflictError(msg)\n\n    _add_optional_dependencies(requirements, optional_dependencies)\n\n    prepared = _prepare_specs_for_conflict_resolution(requirements)\n    for data in prepared.values():\n        _pop_unused_platforms_and_maybe_expand_none(data, platforms)\n    return {\n        pkg: _combine_pinning_within_platform(data) for pkg, data in prepared.items()\n    }\n\n\ndef _parse_pinning(pinning: str) -> tuple[str, version.Version]:\n    \"\"\"Separates the operator and the version number.\"\"\"\n    pinning = pinning.strip()\n    operator = extract_version_operator(pinning)\n    if operator and operator in VALID_OPERATORS:\n        version_part = pinning[len(operator) :].strip()\n        if version_part:\n            try:\n                return operator, version.parse(version_part)\n            except version.InvalidVersion:\n                pass\n\n    msg = f\"Invalid version pinning: '{pinning}', must start with one of {VALID_OPERATORS}\"  # noqa: E501\n    raise VersionConflictError(msg)\n\n\ndef _is_redundant(pinning: str, other_pinnings: list[str]) -> bool:\n    \"\"\"Determines if a version pinning is redundant given a list of other pinnings.\"\"\"\n    op, version = _parse_pinning(pinning)\n\n    for other in other_pinnings:\n        other_op, other_version = _parse_pinning(other)\n        if other == pinning:\n            continue\n\n        if op == \"<\" and (\n            (other_op == \"<\" and version >= other_version)\n            or (other_op == \"<=\" and version > other_version)\n        ):\n            return True\n        if op == \"<=\" and other_op in [\"<\", \"<=\"] and version >= other_version:\n            return True\n        if op == \">\" and (\n            (other_op == \">\" and version <= other_version)\n            or (other_op == \">=\" and version < other_version)\n        ):\n            return True\n        if op == \">=\" and other_op in [\">\", \">=\"] and version <= other_version:\n            return True\n\n    return False\n\n\ndef _is_valid_pinning(pinning: str) -> bool:\n    \"\"\"Checks if a version pinning string is valid.\"\"\"\n    if any(op in pinning for op in VALID_OPERATORS):\n        try:\n            # Attempt to parse the version part of the pinning\n            _parse_pinning(pinning)\n            return True  # noqa: TRY300\n        except VersionConflictError:\n            # If parsing fails, the pinning is not valid\n            return False\n    # If the pinning doesn't contain any recognized operator, it's not valid\n    return False\n\n\ndef _deduplicate(pinnings: list[str]) -> list[str]:\n    \"\"\"Removes duplicate strings.\"\"\"\n    return list(dict.fromkeys(pinnings))  # preserve order\n\n\ndef _split_pinnings(pinnings: list[str]) -> list[str]:\n    \"\"\"Extracts version pinnings from a list of Spec objects.\"\"\"\n    return [_pin.lstrip().rstrip() for pin in pinnings for _pin in pin.split(\",\")]\n\n\ndef combine_version_pinnings(pinnings: list[str], *, name: str | None = None) -> str:\n    \"\"\"Combines a list of version pinnings into a single string.\"\"\"\n    pinnings = [p for p in pinnings if p != \"\"]\n    pinnings = _split_pinnings(pinnings)\n    pinnings = _deduplicate(pinnings)\n    if len(pinnings) == 1:\n        return pinnings[0]\n    for pin in pinnings:\n        if not _is_valid_pinning(pin):\n            ops = \", \".join(VALID_OPERATORS)\n            url = f\"{_REPO_URL}/blob/main/README.md#supported-version-pinnings\"\n            msg = (\n                f\"Invalid version pinning '{pin}' for '{name}'. \"\n                \"UniDep supports only the following operators for combining pinnings: \"\n                f\"{ops}. For complex pinnings (like VCS URLs, local paths, or build\"\n                \" strings), ensure all pinnings are identical. Divergent complex\"\n                f\" pinnings cannot be combined. See {url} for more information.\"\n            )\n\n            raise VersionConflictError(msg)\n\n    valid_pinnings = [p.replace(\" \", \"\") for p in pinnings]\n    exact_pinnings = [p for p in valid_pinnings if p.startswith(\"=\")]\n    if len(exact_pinnings) > 1:\n        pinnings_str = \", \".join(exact_pinnings)\n        msg = f\"Multiple exact version pinnings found: {pinnings_str} for `{name}`\"\n        raise VersionConflictError(msg)\n\n    err_msg = f\"Contradictory version pinnings found for `{name}`\"\n\n    if exact_pinnings:\n        exact_pin = exact_pinnings[0]\n        exact_version = version.parse(exact_pin[1:])\n        for other_pin in valid_pinnings:\n            if other_pin != exact_pin:\n                op, ver = _parse_pinning(other_pin)\n                if not (\n                    (op == \"<\" and exact_version < ver)\n                    or (op == \"<=\" and exact_version <= ver)\n                    or (op == \">\" and exact_version > ver)\n                    or (op == \">=\" and exact_version >= ver)\n                ):\n                    msg = f\"{err_msg}: {exact_pin} and {other_pin}\"\n                    raise VersionConflictError(msg)\n        return exact_pin\n\n    non_redundant_pinnings = [\n        pin for pin in valid_pinnings if not _is_redundant(pin, valid_pinnings)\n    ]\n\n    for i, pin in enumerate(non_redundant_pinnings):\n        for other_pin in non_redundant_pinnings[i + 1 :]:\n            op1, ver1 = _parse_pinning(pin)\n            op2, ver2 = _parse_pinning(other_pin)\n            msg = f\"{err_msg}: {pin} and {other_pin}\"\n            # Check for direct contradictions like >2 and <1\n            if (op1 == \">\" and op2 == \"<\" and ver1 >= ver2) or (\n                op1 == \"<\" and op2 == \">\" and ver1 <= ver2\n            ):\n                raise VersionConflictError(msg)\n\n            # Check for contradictions involving inclusive bounds like >=2 and <1\n            if (\n                (op1 == \">=\" and op2 == \"<\" and ver1 >= ver2)\n                or (op1 == \">\" and op2 == \"<=\" and ver1 >= ver2)\n                or (op1 == \"<=\" and op2 == \">\" and ver1 <= ver2)\n                or (op1 == \">\" and op2 == \"<=\" and ver1 >= ver2)\n            ):\n                raise VersionConflictError(msg)\n\n    return \",\".join(non_redundant_pinnings)\n"
  },
  {
    "path": "unidep/_dependencies_parsing.py",
    "content": "\"\"\"unidep - Unified Conda and Pip requirements management.\n\nThis module provides parsing of `requirements.yaml` and `pyproject.toml` files.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport functools\nimport hashlib\nimport os\nimport sys\nfrom collections import defaultdict\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, NamedTuple, cast\n\nfrom ruamel.yaml import YAML\nfrom ruamel.yaml.comments import CommentedMap, CommentedSeq\n\nfrom unidep.platform_definitions import Platform, Spec, platforms_from_selector\nfrom unidep.utils import (\n    LocalDependency,\n    LocalDependencyUse,\n    PathWithExtras,\n    defaultdict_to_dict,\n    is_pip_installable,\n    parse_folder_or_filename,\n    parse_package_str,\n    selector_from_comment,\n    split_path_and_extras,\n    unidep_configured_in_toml,\n    warn,\n)\n\nif TYPE_CHECKING:\n    if sys.version_info >= (3, 8):\n        from typing import Literal\n    else:  # pragma: no cover\n        from typing_extensions import Literal\n\n\nif sys.version_info >= (3, 11):\n    import tomllib\nelse:  # pragma: no cover\n    import tomli as tomllib\n\n\ndef find_requirements_files(\n    base_dir: str | Path = \".\",\n    depth: int = 1,\n    *,\n    verbose: bool = False,\n) -> list[Path]:\n    \"\"\"Scan a directory for `requirements.yaml` and `pyproject.toml` files.\"\"\"\n    base_path = Path(base_dir)\n    found_files = []\n\n    # Define a helper function to recursively scan directories\n    def _scan_dir(path: Path, current_depth: int) -> None:\n        if verbose:\n            print(f\"🔍 Scanning in `{path}` at depth {current_depth}\")\n        if current_depth > depth:\n            return\n        for child in sorted(path.iterdir()):\n            if child.is_dir():\n                _scan_dir(child, current_depth + 1)\n            elif child.name == \"requirements.yaml\":\n                found_files.append(child)\n                if verbose:\n                    print(f'🔍 Found `\"requirements.yaml\"` at `{child}`')\n            elif child.name == \"pyproject.toml\" and unidep_configured_in_toml(child):\n                if verbose:\n                    print(f'🔍 Found `\"pyproject.toml\"` with dependencies at `{child}`')\n                found_files.append(child)\n\n    _scan_dir(base_path, 0)\n    return sorted(found_files)\n\n\ndef _extract_first_comment(\n    commented_map: CommentedMap,\n    index_or_key: int | str,\n) -> str | None:\n    \"\"\"Extract the first comment from a CommentedMap.\"\"\"\n    comments = commented_map.ca.items.get(index_or_key, None)\n    if comments is None:\n        return None\n    comment_strings = next(\n        c.value.split(\"\\n\")[0].rstrip().lstrip() for c in comments if c is not None\n    )\n    if not comment_strings:\n        # empty string\n        return None\n    return \"\".join(comment_strings)\n\n\ndef _identifier(identifier: int, selector: str | None) -> str:\n    \"\"\"Return a unique identifier based on the comment.\"\"\"\n    platforms = None if selector is None else tuple(platforms_from_selector(selector))\n    data_str = f\"{identifier}-{platforms}\"\n    # Hash using SHA256 and take the first 8 characters for a shorter hash\n    return hashlib.sha256(data_str.encode()).hexdigest()[:8]\n\n\ndef _parse_dependency(\n    dependency: str,\n    dependencies: CommentedMap,\n    index_or_key: int | str,\n    which: Literal[\"conda\", \"pip\", \"both\"],\n    identifier: int,\n    ignore_pins: list[str],\n    overwrite_pins: dict[str, str | None],\n    skip_dependencies: list[str],\n) -> list[Spec]:\n    name, pin, selector = parse_package_str(dependency)\n    if name in ignore_pins:\n        pin = None\n    if name in skip_dependencies:\n        return []\n    if name in overwrite_pins:\n        pin = overwrite_pins[name]\n    comment = (\n        _extract_first_comment(dependencies, index_or_key)\n        if isinstance(dependencies, (CommentedMap, CommentedSeq))\n        else None\n    )\n    if comment and selector is None:\n        selector = selector_from_comment(comment)\n    identifier_hash = _identifier(identifier, selector)\n    if which == \"both\":\n        return [\n            Spec(name, \"conda\", pin, identifier_hash, selector),\n            Spec(name, \"pip\", pin, identifier_hash, selector),\n        ]\n    return [Spec(name, which, pin, identifier_hash, selector)]\n\n\nclass DependencyOrigin(NamedTuple):\n    \"\"\"Origin information for a parsed dependency entry.\"\"\"\n\n    source_file: Path\n    dependency_index: int\n    optional_group: str | None = None\n    local_dependency_chain: tuple[Path, ...] = ()\n\n\nclass DependencyEntry(NamedTuple):\n    \"\"\"One original dependency declaration with optional conda/pip alternatives.\"\"\"\n\n    identifier: str\n    selector: str | None\n    conda: Spec | None\n    pip: Spec | None\n    origin: DependencyOrigin\n\n\nclass ParsedRequirements(NamedTuple):\n    \"\"\"Requirements with comments.\"\"\"\n\n    channels: list[str]\n    platforms: list[Platform]\n    requirements: dict[str, list[Spec]]\n    optional_dependencies: dict[str, dict[str, list[Spec]]]\n    dependency_entries: list[DependencyEntry]\n    optional_dependency_entries: dict[str, list[DependencyEntry]]\n    pip_indices: tuple[str, ...] = ()\n\n\nclass Requirements(NamedTuple):\n    \"\"\"Requirements as CommentedSeq.\"\"\"\n\n    # mypy doesn't support CommentedSeq[str], so we use list[str] instead.\n    channels: list[str]  # actually a CommentedSeq[str]\n    conda: list[str]  # actually a CommentedSeq[str]\n    pip: list[str]  # actually a CommentedSeq[str]\n\n\nclass _LoadedRequirementData(NamedTuple):\n    data: dict[str, Any]\n    path_with_extras: PathWithExtras\n    local_dependency_chain: tuple[Path, ...]\n\n\ndef _parse_overwrite_pins(overwrite_pins: list[str]) -> dict[str, str | None]:\n    \"\"\"Parse overwrite pins.\"\"\"\n    result = {}\n    for overwrite_pin in overwrite_pins:\n        pkg = parse_package_str(overwrite_pin)\n        result[pkg.name] = pkg.pin\n    return result\n\n\ndef _collect_pip_indices(data: dict[str, Any]) -> list[str]:\n    \"\"\"Collect pip index URLs from the unidep config.\"\"\"\n    indices: list[str] = []\n    if \"pip_indices\" not in data:\n        return indices\n    value = data[\"pip_indices\"]\n    if isinstance(value, str):\n        values = [value]\n    elif isinstance(value, list):\n        values = value\n    else:\n        msg = \"`pip_indices` must be a string or a list of strings.\"\n        raise TypeError(msg)\n    for index in values:\n        if not isinstance(index, str):\n            msg = \"`pip_indices` entries must be strings.\"\n            raise TypeError(msg)\n        if index and index not in indices:\n            indices.append(index)\n    return indices\n\n\n@functools.lru_cache\ndef _load(p: Path, yaml: YAML) -> dict[str, Any]:\n    if p.suffix == \".toml\":\n        with p.open(\"rb\") as f:\n            pyproject = tomllib.load(f)\n            project_dependencies = pyproject.get(\"project\", {}).get(\"dependencies\", [])\n            unidep_cfg = pyproject[\"tool\"][\"unidep\"]\n            if not project_dependencies:\n                return unidep_cfg\n            unidep_dependencies = unidep_cfg.setdefault(\"dependencies\", [])\n            project_dependency_handling = unidep_cfg.get(\n                \"project_dependency_handling\",\n                \"ignore\",\n            )\n            _add_project_dependencies(\n                project_dependencies,\n                unidep_dependencies,\n                project_dependency_handling,\n            )\n            return unidep_cfg\n    with p.open() as f:\n        return yaml.load(f)\n\n\ndef _add_project_dependencies(\n    project_dependencies: list[str],\n    unidep_dependencies: list[dict[str, str] | str],\n    project_dependency_handling: Literal[\"same-name\", \"pip-only\", \"ignore\"],\n) -> None:\n    \"\"\"Add project dependencies to unidep dependencies based on the chosen handling.\"\"\"\n    if project_dependency_handling == \"same-name\":\n        unidep_dependencies.extend(project_dependencies)\n    elif project_dependency_handling == \"pip-only\":\n        unidep_dependencies.extend([{\"pip\": dep} for dep in project_dependencies])\n    elif project_dependency_handling != \"ignore\":\n        msg = (\n            f\"Invalid `project_dependency_handling` value: {project_dependency_handling}.\"  # noqa: E501\n            \" Must be one of 'same-name', 'pip-only', 'ignore'.\"\n        )\n        raise ValueError(msg)\n\n\ndef _parse_local_dependency_item(item: str | dict[str, str]) -> LocalDependency:\n    \"\"\"Parse a single local dependency item into a LocalDependency object.\"\"\"\n    if isinstance(item, str):\n        return LocalDependency(local=item, pypi=None)\n    if isinstance(item, dict):\n        if \"local\" not in item:\n            msg = \"Dictionary-style local dependency must have a 'local' key\"\n            raise ValueError(msg)\n        use = _normalize_local_dependency_use(item.get(\"use\"))\n        pypi_value = item.get(\"pypi\")\n        if use == \"pypi\" and not pypi_value:\n            msg = \"Local dependency with `use: pypi` must specify a `pypi` alternative.\"\n            raise ValueError(msg)\n        return LocalDependency(local=item[\"local\"], pypi=pypi_value, use=use)\n    msg = f\"Invalid local dependency format: {item}\"\n    raise TypeError(msg)\n\n\ndef _normalize_local_dependency_use(use_value: str | None) -> LocalDependencyUse:\n    if use_value is None:\n        return \"local\"\n    normalized = use_value.strip().lower()\n    valid = {\"local\", \"pypi\", \"skip\"}\n    if normalized not in valid:\n        options = \", \".join(sorted(valid))\n        msg = f\"Invalid `use` value `{use_value}`. Supported values: {options}.\"\n        raise ValueError(msg)\n    return cast(\"LocalDependencyUse\", normalized)\n\n\ndef get_local_dependencies(data: dict[str, Any]) -> list[LocalDependency]:\n    \"\"\"Get `local_dependencies` from a `requirements.yaml` or `pyproject.toml` file.\"\"\"\n    raw_deps = []\n    if \"local_dependencies\" in data:\n        raw_deps = data[\"local_dependencies\"]\n    elif \"includes\" in data:\n        warn(\n            \"⚠️ You are using `includes` in `requirements.yaml` or `pyproject.toml`\"\n            \" `[unidep.tool]` which is deprecated since 0.42.0 and has been renamed to\"\n            \" `local_dependencies`.\",\n            category=DeprecationWarning,\n            stacklevel=2,\n        )\n        raw_deps = data[\"includes\"]\n\n    return [_parse_local_dependency_item(item) for item in raw_deps]\n\n\ndef _to_path_with_extras(\n    paths: list[Path],\n    extras: list[list[str]] | Literal[\"*\"] | None,\n) -> list[PathWithExtras]:\n    if isinstance(extras, (list, tuple)) and len(extras) != len(paths):\n        msg = (\n            f\"Length of `extras` ({len(extras)}) does not match length\"\n            f\" of `paths` ({len(paths)}).\"\n        )\n        raise ValueError(msg)\n    paths_with_extras = [parse_folder_or_filename(p) for p in paths]\n    if extras is None:\n        return paths_with_extras\n    assert extras is not None\n    if any(p.extras for p in paths_with_extras):\n        msg = (\n            \"Cannot specify `extras` list when paths are\"\n            \" specified like `path/to/project[extra1,extra2]`, `extras` must be `None`\"\n            \" or specify pure paths without extras like `path/to/project` and specify\"\n            \" extras in `extras`.\"\n        )\n        raise ValueError(msg)\n    if extras == \"*\":\n        extras = [[\"*\"]] * len(paths)  # type: ignore[list-item]\n\n    return [PathWithExtras(p.path, e) for p, e in zip(paths_with_extras, extras)]\n\n\ndef _update_data_structures(\n    *,\n    path_with_extras: PathWithExtras,\n    loaded_data: list[_LoadedRequirementData],  # modified in place\n    all_extras: list[list[str]],  # modified in place\n    seen: set[PathWithExtras],  # modified in place\n    yaml: YAML,\n    is_nested: bool,\n    local_dependency_overrides: dict[Path, LocalDependency],\n    local_dependency_chain: tuple[Path, ...] = (),\n    include_local_dependencies: bool = True,\n    verbose: bool = False,\n) -> None:\n    if verbose:\n        print(f\"📄 Parsing `{path_with_extras.path_with_extras}`\")\n    data = _load(path_with_extras.path, yaml)\n    loaded_data.append(\n        _LoadedRequirementData(\n            data=data,\n            path_with_extras=path_with_extras,\n            local_dependency_chain=local_dependency_chain,\n        ),\n    )\n    _move_local_optional_dependencies_to_local_dependencies(\n        data=data,  # modified in place\n        path_with_extras=path_with_extras,\n        verbose=verbose,\n    )\n    if not is_nested:\n        all_extras.append(path_with_extras.extras)\n    else:\n        # When nested, the extras that are specified in the\n        # local_dependencies section should be moved to the main dependencies\n        # because they are not optional if specified in the file. Only\n        # the top-level extras are optional.\n        all_extras.append([])\n        _move_optional_dependencies_to_dependencies(\n            data=data,  # modified in place\n            path_with_extras=path_with_extras,\n            verbose=verbose,\n        )\n\n    seen.add(path_with_extras.resolved())\n\n    # Handle \"local_dependencies\" (or old name \"includes\", changed in 0.42.0)\n    for effective_local_dep in _effective_local_dependencies(\n        data=data,\n        base_dir=path_with_extras.path.parent,\n        overrides=local_dependency_overrides,\n    ):\n        if effective_local_dep.use == \"skip\":\n            continue\n        if effective_local_dep.use == \"pypi\":\n            _append_pip_dependency_from_local(\n                data=data,\n                local_dependency=effective_local_dep,\n            )\n            continue\n        if not include_local_dependencies:\n            continue\n        # NOTE: The current function calls _add_local_dependencies,\n        # which calls the current function recursively\n        _add_local_dependencies(\n            local_dependency=effective_local_dep.local,\n            path_with_extras=path_with_extras,\n            loaded_data=loaded_data,  # modified in place\n            all_extras=all_extras,  # modified in place\n            seen=seen,  # modified in place\n            yaml=yaml,\n            local_dependency_overrides=local_dependency_overrides,\n            local_dependency_chain=(\n                *local_dependency_chain,\n                path_with_extras.path.resolve(),\n            ),\n            include_local_dependencies=include_local_dependencies,\n            verbose=verbose,\n        )\n\n\ndef _move_optional_dependencies_to_dependencies(\n    data: dict[str, Any],\n    path_with_extras: PathWithExtras,\n    *,\n    verbose: bool = False,\n) -> None:\n    optional_dependencies = data.pop(\"optional_dependencies\", {})\n    for extra in path_with_extras.extras:\n        if extra == \"*\":\n            # If \"*\" is specified, include all optional dependencies\n            for opt_deps in optional_dependencies.values():\n                data.setdefault(\"dependencies\", []).extend(opt_deps)\n            if verbose:\n                print(\n                    \"📄 Moving all optional dependencies to main dependencies\"\n                    f\" for `{path_with_extras.path_with_extras}`\",\n                )\n        elif extra in optional_dependencies:\n            data.setdefault(\"dependencies\", []).extend(optional_dependencies[extra])\n            if verbose:\n                print(\n                    f\"📄 Moving `{extra}` optional dependencies to main dependencies\"\n                    f\" for `{path_with_extras.path_with_extras}`\",\n                )\n\n\ndef _move_local_optional_dependencies_to_local_dependencies(\n    *,\n    data: dict[str, Any],  # modified in place\n    path_with_extras: PathWithExtras,\n    verbose: bool = False,\n) -> None:\n    # Move local dependencies from `optional_dependencies` to `local_dependencies`\n    extras = path_with_extras.extras\n    if \"*\" in extras:\n        extras = list(data.get(\"optional_dependencies\", {}).keys())\n\n    optional_dependencies = data.get(\"optional_dependencies\", {})\n    for extra in extras:\n        moved = set()\n        for dep in optional_dependencies.get(extra, []):\n            if isinstance(dep, dict):\n                # This is a {\"pip\": \"package\"} and/or {\"conda\": \"package\"} dependency\n                continue\n            if _str_is_path_like(dep):\n                if verbose:\n                    print(\n                        f\"📄 Moving `{dep}` from the `{extra}` section in\"\n                        \" `optional_dependencies` to `local_dependencies`\",\n                    )\n                data.setdefault(\"local_dependencies\", []).append(dep)\n                moved.add(dep)\n        for dep in moved:\n            extras = optional_dependencies[extra]  # key must exist if moved non-empty\n            extras.pop(extras.index(dep))\n\n    # Remove empty optional_dependencies sections\n    to_delete = [extra for extra, deps in optional_dependencies.items() if not deps]\n    for extra in to_delete:\n        if verbose:\n            print(f\"📄 Removing empty `{extra}` section from `optional_dependencies`\")\n        optional_dependencies.pop(extra)\n\n\ndef _resolve_local_dependency_path(base_dir: Path, local: str) -> Path:\n    local_path, _ = split_path_and_extras(local)\n    return (base_dir / local_path).resolve()\n\n\ndef _try_parse_local_dependency_requirement_file(\n    *,\n    base_dir: Path,\n    local_dependency: str,\n) -> PathWithExtras | None:\n    \"\"\"Return managed requirements file for a local dependency, if present.\"\"\"\n    try:\n        requirements_dep_file = parse_folder_or_filename(base_dir / local_dependency)\n    except FileNotFoundError:\n        return None\n    if requirements_dep_file.path.suffix in (\".whl\", \".zip\"):\n        return None\n    return requirements_dep_file\n\n\ndef _apply_local_dependency_override(\n    *,\n    local_dependency: LocalDependency,\n    base_dir: Path,\n    overrides: dict[Path, LocalDependency],\n) -> LocalDependency:\n    try:\n        resolved_path = _resolve_local_dependency_path(base_dir, local_dependency.local)\n    except (OSError, RuntimeError, ValueError):  # pragma: no cover\n        resolved_path = None\n    if local_dependency.use != \"local\" and resolved_path is not None:\n        overrides[resolved_path] = local_dependency\n        return local_dependency\n    if (\n        local_dependency.use == \"local\"\n        and resolved_path is not None\n        and resolved_path in overrides\n    ):\n        override = overrides[resolved_path]\n        return LocalDependency(\n            local=local_dependency.local,\n            pypi=local_dependency.pypi or override.pypi,\n            use=override.use,\n        )\n    return local_dependency\n\n\ndef _effective_local_dependencies(\n    *,\n    data: dict[str, Any],\n    base_dir: Path,\n    overrides: dict[Path, LocalDependency],\n) -> list[LocalDependency]:\n    \"\"\"Return local dependencies after applying global ``use`` overrides.\"\"\"\n    local_dependencies = get_local_dependencies(data)\n    for local_dep_obj in local_dependencies:\n        if local_dep_obj.use != \"local\":\n            _apply_local_dependency_override(\n                local_dependency=local_dep_obj,\n                base_dir=base_dir,\n                overrides=overrides,\n            )\n    return [\n        _apply_local_dependency_override(\n            local_dependency=local_dep_obj,\n            base_dir=base_dir,\n            overrides=overrides,\n        )\n        for local_dep_obj in local_dependencies\n    ]\n\n\ndef _append_pip_dependency_from_local(\n    *,\n    data: dict[str, Any],\n    local_dependency: LocalDependency,\n) -> None:\n    assert local_dependency.pypi is not None\n    dependency_entry: str | dict[str, str]\n    dependency_entry = {\"pip\": local_dependency.pypi}\n    data.setdefault(\"dependencies\", []).append(dependency_entry)\n\n\ndef _add_local_dependencies(\n    *,\n    local_dependency: str,\n    path_with_extras: PathWithExtras,\n    loaded_data: list[_LoadedRequirementData],\n    all_extras: list[list[str]],\n    seen: set[PathWithExtras],\n    yaml: YAML,\n    local_dependency_overrides: dict[Path, LocalDependency],\n    local_dependency_chain: tuple[Path, ...] = (),\n    include_local_dependencies: bool = True,\n    verbose: bool = False,\n) -> None:\n    requirements_dep_file = _try_parse_local_dependency_requirement_file(\n        base_dir=path_with_extras.path.parent,\n        local_dependency=local_dependency,\n    )\n    if requirements_dep_file is None:\n        local_path, _ = split_path_and_extras(local_dependency)\n        abs_local = (path_with_extras.path.parent / local_path).resolve()\n        if verbose and abs_local.suffix in (\".whl\", \".zip\") and abs_local.exists():\n            print(\n                f\"⚠️  Local dependency `{local_dependency}` is a wheel or zip file. \"\n                \"Skipping parsing, but it will be installed by pip if \"\n                \"`--skip-local` is not set. Note that unidep will not \"\n                \"detect its dependencies.\",\n            )\n        return\n    if requirements_dep_file.resolved() in seen:\n        return  # Avoids circular local_dependencies\n    if verbose:\n        print(f\"📄 Parsing `{local_dependency}` from `local_dependencies`\")\n    _update_data_structures(\n        path_with_extras=requirements_dep_file,\n        loaded_data=loaded_data,  # modified in place\n        all_extras=all_extras,  # modified in place\n        seen=seen,  # modified in place\n        yaml=yaml,\n        verbose=verbose,\n        is_nested=True,\n        local_dependency_overrides=local_dependency_overrides,\n        local_dependency_chain=local_dependency_chain,\n        include_local_dependencies=include_local_dependencies,\n    )\n\n\ndef parse_requirements(\n    *paths: Path,\n    ignore_pins: list[str] | None = None,\n    overwrite_pins: list[str] | None = None,\n    skip_dependencies: list[str] | None = None,\n    verbose: bool = False,\n    extras: list[list[str]] | Literal[\"*\"] | None = None,\n    include_local_dependencies: bool = True,\n) -> ParsedRequirements:\n    \"\"\"Parse a list of `requirements.yaml` or `pyproject.toml` files.\n\n    Parameters\n    ----------\n    paths\n        Paths to `requirements.yaml` or `pyproject.toml` files.\n    ignore_pins\n        List of package names to ignore pins for.\n    overwrite_pins\n        List of package names with pins to overwrite.\n    skip_dependencies\n        List of package names to skip.\n    verbose\n        Whether to print verbose output.\n    extras\n        List of lists of extras to include. The outer list corresponds to the\n        `requirements.yaml` or `pyproject.toml` files, the inner list to the\n        extras to include for that file. If \"*\", all extras are included,\n        if None, no extras are included.\n    include_local_dependencies\n        Whether local dependencies should be recursively parsed and merged into\n        the result. When False, local dependencies with `use: pypi` are still\n        translated to pip dependencies, but `use: local` entries are not\n        traversed.\n\n    \"\"\"\n    paths_with_extras = _to_path_with_extras(paths, extras)  # type: ignore[arg-type]\n    ignore_pins = ignore_pins or []\n    skip_dependencies = skip_dependencies or []\n    overwrite_pins_map = _parse_overwrite_pins(overwrite_pins or [])\n\n    # `loaded_data` and `all_extras` are lists of the same length\n    loaded_data: list[_LoadedRequirementData] = []\n    all_extras: list[list[str]] = []\n    seen: set[PathWithExtras] = set()\n    local_dependency_overrides: dict[Path, LocalDependency] = {}\n    yaml = YAML(typ=\"rt\")  # Might be unused if all are TOML files\n    for path_with_extras in paths_with_extras:\n        _update_data_structures(\n            path_with_extras=path_with_extras,\n            loaded_data=loaded_data,  # modified in place\n            all_extras=all_extras,  # modified in place\n            seen=seen,  # modified in place\n            yaml=yaml,\n            verbose=verbose,\n            is_nested=False,\n            local_dependency_overrides=local_dependency_overrides,\n            include_local_dependencies=include_local_dependencies,\n        )\n\n    assert len(loaded_data) == len(all_extras)\n\n    # Parse the requirements from loaded data\n    requirements: dict[str, list[Spec]] = defaultdict(list)\n    optional_dependencies: dict[str, dict[str, list[Spec]]] = defaultdict(\n        lambda: defaultdict(list),\n    )\n    dependency_entries: list[DependencyEntry] = []\n    optional_dependency_entries: dict[str, list[DependencyEntry]] = defaultdict(list)\n    channels: set[str] = set()\n    pip_indices: list[str] = []  # Preserve order, first is primary\n    platforms: set[Platform] = set()\n\n    identifier = -1\n    for loaded, _extras in zip(loaded_data, all_extras):\n        data = loaded.data\n        channels.update(data.get(\"channels\", []))\n        # Collect pip_indices, maintaining order and avoiding duplicates.\n        for index in _collect_pip_indices(data):\n            if index and index not in pip_indices:\n                pip_indices.append(index)\n        platforms.update(data.get(\"platforms\", []))\n        if \"dependencies\" in data:\n            identifier = _add_dependencies(\n                data[\"dependencies\"],\n                requirements,  # modified in place\n                dependency_entries,  # modified in place\n                identifier,\n                ignore_pins,\n                overwrite_pins_map,\n                skip_dependencies,\n                source_file=loaded.path_with_extras.path,\n                local_dependency_chain=loaded.local_dependency_chain,\n            )\n        for opt_name, opt_deps in data.get(\"optional_dependencies\", {}).items():\n            if opt_name in _extras or \"*\" in _extras:\n                identifier = _add_dependencies(\n                    opt_deps,\n                    optional_dependencies[opt_name],  # modified in place\n                    optional_dependency_entries[opt_name],  # modified in place\n                    identifier,\n                    ignore_pins,\n                    overwrite_pins_map,\n                    skip_dependencies,\n                    is_optional=True,\n                    optional_group=opt_name,\n                    source_file=loaded.path_with_extras.path,\n                    local_dependency_chain=loaded.local_dependency_chain,\n                )\n\n    return ParsedRequirements(\n        sorted(channels),\n        sorted(platforms),\n        dict(requirements),\n        defaultdict_to_dict(optional_dependencies),\n        dependency_entries,\n        defaultdict_to_dict(optional_dependency_entries),\n        tuple(pip_indices),\n    )\n\n\ndef _str_is_path_like(s: str) -> bool:\n    \"\"\"Check if a string is path-like.\"\"\"\n    return os.path.sep in s or \"/\" in s or s.startswith(\".\")\n\n\ndef _check_allowed_local_dependency(name: str, is_optional: bool) -> None:  # noqa: FBT001\n    if _str_is_path_like(name):\n        # There should not be path-like dependencies in the optional_dependencies\n        # section after _move_local_optional_dependencies_to_local_dependencies.\n        assert not is_optional\n        msg = (\n            f\"Local dependencies (`{name}`) are not allowed in `dependencies`.\"\n            \" Use the `local_dependencies` section instead.\"\n        )\n        raise ValueError(msg)\n\n\ndef _add_dependencies(\n    dependencies: list[str],\n    requirements: dict[str, list[Spec]],  # modified in place\n    dependency_entries: list[DependencyEntry],  # modified in place\n    identifier: int,\n    ignore_pins: list[str],\n    overwrite_pins_map: dict[str, str | None],\n    skip_dependencies: list[str],\n    *,\n    is_optional: bool = False,\n    optional_group: str | None = None,\n    source_file: Path,\n    local_dependency_chain: tuple[Path, ...] = (),\n) -> int:\n    for i, dep in enumerate(dependencies):\n        identifier += 1\n        origin = DependencyOrigin(\n            source_file=source_file,\n            dependency_index=i + 1,\n            optional_group=optional_group,\n            local_dependency_chain=local_dependency_chain,\n        )\n        if isinstance(dep, str):\n            specs = _parse_dependency(\n                dep,\n                dependencies,\n                i,\n                \"both\",\n                identifier,\n                ignore_pins,\n                overwrite_pins_map,\n                skip_dependencies,\n            )\n            if not specs:\n                continue\n            for spec in specs:\n                _check_allowed_local_dependency(spec.name, is_optional)\n                requirements[spec.name].append(spec)\n            dependency_entries.append(\n                DependencyEntry(\n                    identifier=specs[0].identifier\n                    or _identifier(identifier, specs[0].selector),\n                    selector=specs[0].selector,\n                    conda=next((spec for spec in specs if spec.which == \"conda\"), None),\n                    pip=next((spec for spec in specs if spec.which == \"pip\"), None),\n                    origin=origin,\n                ),\n            )\n            continue\n        assert isinstance(dep, dict)\n        conda_spec: Spec | None = None\n        pip_spec: Spec | None = None\n        for which in [\"conda\", \"pip\"]:\n            if which in dep:\n                specs = _parse_dependency(\n                    dep[which],\n                    dep,\n                    which,\n                    which,  # type: ignore[arg-type]\n                    identifier,\n                    ignore_pins,\n                    overwrite_pins_map,\n                    skip_dependencies,\n                )\n                if not specs:\n                    continue\n                for spec in specs:\n                    _check_allowed_local_dependency(spec.name, is_optional)\n                    requirements[spec.name].append(spec)\n                    if spec.which == \"conda\":\n                        conda_spec = spec\n                    else:\n                        pip_spec = spec\n        if conda_spec is not None or pip_spec is not None:\n            identifier_hash = (\n                conda_spec.identifier if conda_spec is not None else pip_spec.identifier  # type: ignore[union-attr]\n            )\n            selector = (\n                conda_spec.selector\n                if conda_spec is not None and conda_spec.selector is not None\n                else pip_spec.selector\n                if pip_spec is not None\n                else None\n            )\n            dependency_entries.append(\n                DependencyEntry(\n                    identifier=identifier_hash or _identifier(identifier, selector),\n                    selector=selector,\n                    conda=conda_spec,\n                    pip=pip_spec,\n                    origin=origin,\n                ),\n            )\n    return identifier\n\n\n# Alias for backwards compatibility\nparse_yaml_requirements = parse_requirements\n\n\ndef _extract_local_dependencies(  # noqa: PLR0912\n    path: Path,\n    base_path: Path,\n    processed: set[Path],\n    dependencies: dict[str, set[str]],\n    *,\n    check_pip_installable: bool = True,\n    verbose: bool = False,\n    raise_if_missing: bool = True,\n    warn_non_managed: bool = True,\n    local_dependency_overrides: dict[Path, LocalDependency],\n) -> None:\n    path, extras = parse_folder_or_filename(path)\n    if path in processed:\n        return\n    processed.add(path)\n    yaml = YAML(typ=\"safe\")\n    data = _load(path, yaml)\n    _move_local_optional_dependencies_to_local_dependencies(\n        data=data,  # modified in place\n        path_with_extras=PathWithExtras(path, extras),\n        verbose=verbose,\n    )\n    for effective_local_dep in _effective_local_dependencies(\n        data=data,\n        base_dir=path.parent,\n        overrides=local_dependency_overrides,\n    ):\n        if effective_local_dep.use != \"local\":\n            continue\n        local_dependency = effective_local_dep.local\n        assert not os.path.isabs(local_dependency)  # noqa: PTH117\n        local_path, extras = split_path_and_extras(local_dependency)\n        abs_local = (path.parent / local_path).resolve()\n        if abs_local.suffix in (\".whl\", \".zip\"):\n            if verbose:\n                print(f\"🔗 Adding `{local_dependency}` from `local_dependencies`\")\n            dependencies[str(base_path)].add(str(abs_local))\n            continue\n        if not abs_local.exists():\n            if raise_if_missing:\n                msg = f\"File `{abs_local}` not found.\"\n                raise FileNotFoundError(msg)\n            continue\n\n        try:\n            requirements_path = parse_folder_or_filename(abs_local).path\n        except FileNotFoundError:\n            # Means that this is a local package that is not managed by unidep.\n            if is_pip_installable(abs_local):\n                dependencies[str(base_path)].add(str(abs_local))\n                if warn_non_managed:\n                    # We do not need to emit this warning when `pip install` is called\n                    warn(\n                        f\"⚠️ Installing a local dependency (`{abs_local.name}`) which\"\n                        \" is not managed by unidep, this will skip all of its\"\n                        \" dependencies, i.e., it will call `pip install` with\"\n                        \"  `--no-deps`. To properly manage this dependency,\"\n                        \" add a `requirements.yaml` or `pyproject.toml` file with\"\n                        \" `[tool.unidep]` in its directory.\",\n                    )\n            elif _is_empty_folder(abs_local):\n                msg = (\n                    f\"`{local_dependency}` in `local_dependencies` is not pip\"\n                    \" installable because it is an empty folder. Is it perhaps\"\n                    \" an uninitialized Git submodule? If so, initialize it with\"\n                    \" `git submodule update --init --recursive`. Otherwise,\"\n                    \" remove it from `local_dependencies`.\"\n                )\n                raise RuntimeError(msg) from None\n            elif _is_empty_git_submodule(abs_local):\n                # Extra check for empty Git submodules (common problem folks run into)\n                msg = (\n                    f\"`{local_dependency}` in `local_dependencies` is not installable\"\n                    \" by pip because it is an empty Git submodule. Either remove it\"\n                    \" from `local_dependencies` or fetch the submodule with\"\n                    \" `git submodule update --init --recursive`.\"\n                )\n                raise RuntimeError(msg) from None\n            else:\n                msg = (\n                    f\"`{local_dependency}` in `local_dependencies` is not pip\"\n                    \" installable nor is it managed by unidep. Remove it\"\n                    \" from `local_dependencies`.\"\n                )\n                raise RuntimeError(msg) from None\n            continue\n\n        project_path = str(requirements_path.parent)\n        if project_path == str(base_path):\n            continue\n        if not check_pip_installable or is_pip_installable(requirements_path.parent):\n            dependencies[str(base_path)].add(project_path)\n        if verbose:\n            print(f\"🔗 Adding `{requirements_path}` from `local_dependencies`\")\n        _extract_local_dependencies(\n            requirements_path,\n            base_path,\n            processed,\n            dependencies,\n            check_pip_installable=check_pip_installable,\n            verbose=verbose,\n            raise_if_missing=raise_if_missing,\n            warn_non_managed=warn_non_managed,\n            local_dependency_overrides=local_dependency_overrides,\n        )\n\n\ndef parse_local_dependencies(\n    *paths: Path,\n    check_pip_installable: bool = True,\n    verbose: bool = False,\n    raise_if_missing: bool = True,\n    warn_non_managed: bool = True,\n) -> dict[Path, list[Path]]:\n    \"\"\"Extract local project dependencies from a list of `requirements.yaml` or `pyproject.toml` files.\n\n    Works by loading the specified `local_dependencies` list.\n\n    Returns a dictionary with the:\n    name of the project folder => list of `Path`s of local dependencies folders.\n    \"\"\"  # noqa: E501\n    dependencies: dict[str, set[str]] = defaultdict(set)\n    local_dependency_overrides: dict[Path, LocalDependency] = {}\n\n    for p in paths:\n        if verbose:\n            print(f\"🔗 Analyzing dependencies in `{p}`\")\n        base_path = p.resolve().parent\n        _extract_local_dependencies(\n            path=p,\n            base_path=base_path,\n            processed=set(),\n            dependencies=dependencies,\n            check_pip_installable=check_pip_installable,\n            verbose=verbose,\n            raise_if_missing=raise_if_missing,\n            warn_non_managed=warn_non_managed,\n            local_dependency_overrides=local_dependency_overrides,\n        )\n\n    return {\n        Path(k): sorted({Path(v) for v in v_set})\n        for k, v_set in sorted(dependencies.items())\n    }\n\n\ndef yaml_to_toml(yaml_path: Path) -> str:\n    \"\"\"Converts a `requirements.yaml` file TOML format.\"\"\"\n    try:\n        import tomli_w\n    except ImportError:  # pragma: no cover\n        msg = (\n            \"❌ `tomli_w` is required to convert YAML to TOML.\"\n            \" Install it with `pip install tomli_w`.\"\n        )\n        raise ImportError(msg) from None\n    yaml = YAML(typ=\"rt\")\n    data = _load(yaml_path, yaml)\n    data.pop(\"name\", None)\n    dependencies = data.get(\"dependencies\", [])\n    for i, dep in enumerate(dependencies):\n        if isinstance(dep, str):\n            comment = _extract_first_comment(dependencies, i)\n            if comment is not None:\n                selector = selector_from_comment(comment)\n                if selector is not None:\n                    dependencies[i] = f\"{dep}:{selector}\"\n            continue\n        assert isinstance(dep, dict)\n        for which in [\"conda\", \"pip\"]:\n            if which in dep:\n                comment = _extract_first_comment(dep, which)\n                if comment is not None:\n                    selector = selector_from_comment(comment)\n                    if selector is not None:\n                        dep[which] = f\"{dep[which]}:{selector}\"\n\n    return tomli_w.dumps({\"tool\": {\"unidep\": data}})\n\n\ndef _is_empty_git_submodule(path: Path) -> bool:\n    \"\"\"Checks if the given path is an empty Git submodule.\"\"\"\n    if not path.is_dir():\n        return False\n\n    git_file = path / \".git\"\n    if not git_file.exists() or not git_file.is_file():\n        return False\n\n    # Check if it's empty (apart from the .git file)\n    return len(list(path.iterdir())) == 1  # Only .git should be present\n\n\ndef _is_empty_folder(path: Path) -> bool:\n    \"\"\"Checks if the given path is an empty folder.\"\"\"\n    return not any(path.iterdir())\n"
  },
  {
    "path": "unidep/_dependency_selection.py",
    "content": "\"\"\"Shared conda/pip dependency selection for CLI-facing outputs.\"\"\"\n\nfrom __future__ import annotations\n\nimport math\nfrom dataclasses import dataclass\nfrom typing import TYPE_CHECKING, Optional, Tuple, cast\n\nfrom packaging.specifiers import InvalidSpecifier, Specifier\nfrom packaging.utils import canonicalize_name\nfrom packaging.version import Version\n\nfrom unidep._conflicts import (\n    VersionConflictError,\n    combine_version_pinnings,\n    extract_version_operator,\n)\nfrom unidep.platform_definitions import (\n    PLATFORM_SELECTOR_MAP,\n    CondaPip,\n    Platform,\n    Spec,\n)\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable, Sequence\n\n    from unidep._dependencies_parsing import DependencyEntry, DependencyOrigin\n\nTargetPlatform = Optional[Platform]\nFamilyKey = Tuple[Optional[str], Optional[str]]\n\n\n@dataclass(frozen=True)\nclass SourceRequirement:\n    source: CondaPip\n    spec: Spec\n    family_key: FamilyKey\n    base_name: str\n    normalized_name: str\n    extras: tuple[str, ...]\n    declared_platforms: tuple[Platform, ...] | None\n    origin: DependencyOrigin\n\n\n@dataclass(frozen=True)\nclass MergedSourceCandidate:\n    source: CondaPip\n    spec: Spec\n    normalized_name: str\n    family_keys: tuple[FamilyKey, ...]\n    requirements: tuple[SourceRequirement, ...]\n    declared_scopes: tuple[tuple[Platform, ...] | None, ...]\n\n\n@dataclass(frozen=True)\nclass PlatformCandidates:\n    family_key: FamilyKey\n    platform: TargetPlatform\n    conda: MergedSourceCandidate | None\n    pip: MergedSourceCandidate | None\n\n\ndef _operator_order_key(constraint: str) -> tuple[int, str]:\n    op = extract_version_operator(constraint)\n    order = {\n        \"===\": 0,\n        \"==\": 1,\n        \"~=\": 2,\n        \">=\": 3,\n        \"<=\": 4,\n        \"!=\": 5,\n        \">\": 6,\n        \"<\": 7,\n        \"=\": 8,\n    }\n    return (order.get(op, len(order)), constraint)\n\n\ndef _canonicalize_joined_pinnings(pinnings: list[str]) -> str:\n    seen: set[str] = set()\n    for pinning in pinnings:\n        for stripped in filter(None, (token.strip() for token in pinning.split(\",\"))):\n            seen.add(stripped)\n    return \",\".join(sorted(seen, key=_operator_order_key))\n\n\ndef _parse_pip_name(name: str) -> tuple[str, tuple[str, ...]]:\n    if not name.endswith(\"]\") or \"[\" not in name:\n        return name, ()\n    base_name, extras = name[:-1].split(\"[\", 1)\n    parsed = tuple(sorted(e.strip() for e in extras.split(\",\") if e.strip()))\n    return base_name, parsed\n\n\ndef _build_pip_name(base_name: str, extras: tuple[str, ...]) -> str:\n    if not extras:\n        return base_name\n    return f\"{base_name}[{','.join(extras)}]\"\n\n\ndef _spec_is_pinned(spec: Spec) -> bool:\n    return spec.pin is not None\n\n\ndef _candidate_scope_rank(candidate: MergedSourceCandidate) -> float:\n    ranks = [len(scope) for scope in candidate.declared_scopes if scope is not None]\n    if not ranks:\n        return math.inf\n    return min(ranks)\n\n\ndef _candidate_has_universal_origin(candidate: MergedSourceCandidate) -> bool:\n    return any(scope is None for scope in candidate.declared_scopes)\n\n\ndef _candidate_has_pip_extras(candidate: MergedSourceCandidate) -> bool:\n    return candidate.source == \"pip\" and bool(_parse_pip_name(candidate.spec.name)[1])\n\n\ndef _candidate_display_key(\n    candidate: MergedSourceCandidate,\n) -> tuple[int, str, str]:\n    return (\n        0 if candidate.source == \"conda\" else 1,\n        candidate.normalized_name,\n        candidate.spec.name_with_pin(is_pip=candidate.source == \"pip\"),\n    )\n\n\ndef _origin_to_text(origin: DependencyOrigin) -> str:\n    parts = [origin.source_file.as_posix(), f\"item {origin.dependency_index}\"]\n    if origin.optional_group is not None:\n        parts.append(f\"group {origin.optional_group}\")\n    if origin.local_dependency_chain:\n        chain = \" -> \".join(path.as_posix() for path in origin.local_dependency_chain)\n        parts.append(f\"via {chain}\")\n    return \", \".join(parts)\n\n\ndef _candidate_to_text(candidate: MergedSourceCandidate) -> str:\n    rendered = candidate.spec.name_with_pin(is_pip=candidate.source == \"pip\")\n    origins = \"; \".join(_origin_to_text(req.origin) for req in candidate.requirements)\n    return f\"{candidate.source}: {rendered} ({origins})\"\n\n\ndef _merge_pin_strings(\n    requirements: list[SourceRequirement],\n    *,\n    allow_unsatisfiable_fallback: bool,\n) -> str | None:\n    pinned = [req.spec.pin for req in requirements if req.spec.pin is not None]\n    if not pinned:\n        return None\n    unique = list(dict.fromkeys(pinned))\n    if len(unique) == 1:\n        return unique[0]\n    if allow_unsatisfiable_fallback:\n        exact_pinnings = [\n            pin for pin in unique if _exact_pinning_version_text(pin) is not None\n        ]\n        distinct_exact_versions = {\n            cast(\"str\", _exact_pinning_version_text(pin)) for pin in exact_pinnings\n        }\n        if len(distinct_exact_versions) > 1:\n            pinnings_str = \", \".join(exact_pinnings)\n            msg = (\n                \"Multiple exact version pinnings found: \"\n                f\"{pinnings_str} for `{requirements[0].base_name}`\"\n            )\n            raise VersionConflictError(msg)\n    try:\n        merged = combine_version_pinnings(unique, name=requirements[0].base_name)\n        return _canonicalize_joined_pinnings([merged])\n    except VersionConflictError:\n        if allow_unsatisfiable_fallback and _joined_pinnings_are_safely_satisfiable(\n            unique,\n        ):\n            return _canonicalize_joined_pinnings(unique)\n        raise\n\n\ndef _bump_release_prefix(release: tuple[int, ...], prefix_len: int) -> str:\n    assert 0 < prefix_len <= len(release)\n    bumped = list(release[:prefix_len])\n    bumped[-1] += 1\n    return \".\".join(str(part) for part in bumped)\n\n\ndef _normalize_pinning_token_for_satisfiability(  # noqa: PLR0911\n    pinning: str,\n) -> list[str] | None:\n    try:\n        specifier = Specifier(pinning)\n    except InvalidSpecifier:\n        return None\n\n    operator = specifier.operator\n    version_text = specifier.version\n\n    if operator in {\">\", \">=\", \"<\", \"<=\"}:\n        return [f\"{operator}{version_text}\"]\n\n    if operator == \"!=\":\n        if \"*\" in version_text:\n            return None\n        return [f\"!={version_text}\"]\n\n    if operator == \"==\":\n        if version_text.endswith(\".*\"):\n            prefix = version_text[:-2]\n            parsed = Version(prefix)\n            upper = _bump_release_prefix(parsed.release, len(parsed.release))\n            return [f\">={prefix}\", f\"<{upper}\"]\n        Version(version_text)\n        return [f\"={version_text}\"]\n\n    if operator == \"~=\":\n        parsed = Version(version_text)\n        upper = _bump_release_prefix(parsed.release, len(parsed.release) - 1)\n        return [f\">={version_text}\", f\"<{upper}\"]\n\n    return None\n\n\ndef _parse_supported_pinning(pinning: str) -> tuple[str, Version]:\n    operator = extract_version_operator(pinning)\n    assert operator\n    version_text = pinning[len(operator) :].strip()\n    return operator, Version(version_text)\n\n\ndef _exact_pinning_version_text(pinning: str) -> str | None:\n    operator = extract_version_operator(pinning)\n    if operator not in {\"==\", \"===\", \"=\"}:\n        return None\n    return pinning[len(operator) :].strip()\n\n\ndef _stricter_lower_bound(\n    current: tuple[Version, bool] | None,\n    candidate: tuple[Version, bool],\n) -> tuple[Version, bool]:\n    if current is None:\n        return candidate\n    if candidate[0] > current[0]:\n        return candidate\n    if candidate[0] < current[0]:\n        return current\n    return (current[0], current[1] and candidate[1])\n\n\ndef _stricter_upper_bound(\n    current: tuple[Version, bool] | None,\n    candidate: tuple[Version, bool],\n) -> tuple[Version, bool]:\n    if current is None:\n        return candidate\n    if candidate[0] < current[0]:\n        return candidate\n    if candidate[0] > current[0]:\n        return current\n    return (current[0], current[1] and candidate[1])\n\n\ndef _normalized_pinnings_are_satisfiable(  # noqa: PLR0911, PLR0912\n    pinnings: list[str],\n) -> bool:\n    exact: Version | None = None\n    excluded: set[Version] = set()\n    lower: tuple[Version, bool] | None = None\n    upper: tuple[Version, bool] | None = None\n\n    for pinning in pinnings:\n        operator, parsed_version = _parse_supported_pinning(pinning)\n        if operator == \"=\":\n            assert exact is None or exact == parsed_version\n            exact = parsed_version\n        elif operator == \"!=\":\n            excluded.add(parsed_version)\n        elif operator == \">\":\n            lower = _stricter_lower_bound(lower, (parsed_version, False))\n        elif operator == \">=\":\n            lower = _stricter_lower_bound(lower, (parsed_version, True))\n        elif operator == \"<\":\n            upper = _stricter_upper_bound(upper, (parsed_version, False))\n        elif operator == \"<=\":\n            upper = _stricter_upper_bound(upper, (parsed_version, True))\n\n    if exact is not None:\n        if exact in excluded:\n            return False\n        if lower is not None and (\n            exact < lower[0] or (exact == lower[0] and not lower[1])\n        ):\n            return False\n        return not (\n            upper is not None\n            and (exact > upper[0] or (exact == upper[0] and not upper[1]))\n        )\n\n    if lower is not None and upper is not None:\n        if lower[0] > upper[0]:\n            return False\n        if lower[0] == upper[0]:\n            if not (lower[1] and upper[1]):\n                return False\n            if lower[0] in excluded:\n                return False\n\n    return True\n\n\ndef _joined_pinnings_are_safely_satisfiable(pinnings: list[str]) -> bool:\n    normalized: list[str] = []\n    for pinning in pinnings:\n        for stripped in filter(None, (token.strip() for token in pinning.split(\",\"))):\n            normalized_tokens = _normalize_pinning_token_for_satisfiability(stripped)\n            if normalized_tokens is None:\n                return False\n            normalized.extend(normalized_tokens)\n    return _normalized_pinnings_are_satisfiable(normalized)\n\n\ndef _merge_source_requirements(\n    source: CondaPip,\n    requirements: list[SourceRequirement],\n) -> MergedSourceCandidate:\n    requirements = list(requirements)\n    if source == \"pip\":\n        extras = tuple(\n            sorted({extra for req in requirements for extra in req.extras}),\n        )\n        pin = _merge_pin_strings(\n            requirements,\n            allow_unsatisfiable_fallback=True,\n        )\n        name = _build_pip_name(requirements[0].base_name, extras)\n        spec = Spec(name=name, which=\"pip\", pin=pin)\n        normalized_name = requirements[0].normalized_name\n    else:\n        pin = _merge_pin_strings(\n            requirements,\n            allow_unsatisfiable_fallback=False,\n        )\n        spec = Spec(name=requirements[0].spec.name, which=\"conda\", pin=pin)\n        normalized_name = requirements[0].normalized_name\n    return MergedSourceCandidate(\n        source=source,\n        spec=spec,\n        normalized_name=normalized_name,\n        family_keys=tuple(dict.fromkeys(req.family_key for req in requirements)),\n        requirements=tuple(requirements),\n        declared_scopes=tuple(req.declared_platforms for req in requirements),\n    )\n\n\ndef _entry_family_key(entry: DependencyEntry) -> FamilyKey:\n    conda_name = entry.conda.name if entry.conda is not None else None\n    pip_name = None\n    if entry.pip is not None:\n        base_name, _extras = _parse_pip_name(entry.pip.name)\n        pip_name = canonicalize_name(base_name)\n    return (conda_name, pip_name)\n\n\ndef _source_requirement_from_spec(\n    spec: Spec,\n    *,\n    family_key: FamilyKey,\n    origin: DependencyOrigin,\n    declared_platforms: tuple[Platform, ...] | None,\n) -> SourceRequirement:\n    if spec.which == \"pip\":\n        base_name, extras = _parse_pip_name(spec.name)\n        normalized_name = canonicalize_name(base_name)\n    else:\n        base_name = spec.name\n        extras = ()\n        normalized_name = spec.name\n    return SourceRequirement(\n        source=spec.which,\n        spec=spec,\n        family_key=family_key,\n        base_name=base_name,\n        normalized_name=normalized_name,\n        extras=extras,\n        declared_platforms=declared_platforms,\n        origin=origin,\n    )\n\n\ndef _collect_target_platforms(\n    _entries: Sequence[DependencyEntry],\n    platforms: Sequence[Platform] | None,\n) -> list[TargetPlatform]:\n    if platforms:\n        return cast(\"list[TargetPlatform]\", list(platforms))\n    return cast(\"list[TargetPlatform]\", sorted(PLATFORM_SELECTOR_MAP))\n\n\ndef _entry_targets(\n    spec: Spec,\n    *,\n    target_platforms: Sequence[TargetPlatform],\n) -> tuple[tuple[Platform, ...] | None, list[TargetPlatform]]:\n    declared = spec.platforms()\n    if declared is None:\n        return None, list(target_platforms)\n    targets: list[TargetPlatform] = [\n        platform for platform in declared if platform in target_platforms\n    ]\n    return tuple(declared), targets\n\n\ndef _build_platform_candidates(\n    entries: Sequence[DependencyEntry],\n    platforms: Sequence[Platform] | None = None,\n) -> list[PlatformCandidates]:\n    target_platforms = _collect_target_platforms(entries, platforms)\n    grouped: dict[\n        FamilyKey,\n        dict[TargetPlatform, dict[CondaPip, list[SourceRequirement]]],\n    ] = {}\n    for entry in entries:\n        family_key = _entry_family_key(entry)\n        for spec in (entry.conda, entry.pip):\n            if spec is None:\n                continue\n            declared_platforms, targets = _entry_targets(\n                spec,\n                target_platforms=target_platforms,\n            )\n            source_requirement = _source_requirement_from_spec(\n                spec,\n                family_key=family_key,\n                origin=entry.origin,\n                declared_platforms=declared_platforms,\n            )\n            for platform in targets:\n                grouped.setdefault(family_key, {}).setdefault(platform, {}).setdefault(\n                    spec.which,\n                    [],\n                ).append(source_requirement)\n\n    result: list[PlatformCandidates] = []\n    for family_key, platform_data in grouped.items():\n        for platform, source_lists in sorted(platform_data.items()):\n            conda = None\n            pip = None\n            if source_lists.get(\"conda\"):\n                conda = _merge_source_requirements(\"conda\", source_lists[\"conda\"])\n            if source_lists.get(\"pip\"):\n                pip = _merge_source_requirements(\"pip\", source_lists[\"pip\"])\n            result.append(\n                PlatformCandidates(\n                    family_key=family_key,\n                    platform=platform,\n                    conda=conda,\n                    pip=pip,\n                ),\n            )\n    return result\n\n\ndef _choose_by_precedence(\n    conda: MergedSourceCandidate | None,\n    pip: MergedSourceCandidate | None,\n) -> MergedSourceCandidate | None:\n    if conda is None:\n        return pip\n    if pip is None:\n        return conda\n    if _candidate_has_pip_extras(pip):\n        return pip\n    conda_pinned = _spec_is_pinned(conda.spec)\n    pip_pinned = _spec_is_pinned(pip.spec)\n    if conda_pinned != pip_pinned:\n        return conda if conda_pinned else pip\n    if conda_pinned and pip_pinned:\n        conda_scope = _candidate_scope_rank(conda)\n        pip_scope = _candidate_scope_rank(pip)\n        if conda_scope != pip_scope:\n            return conda if conda_scope < pip_scope else pip\n    return conda\n\n\ndef _select_conda_like_candidate(\n    platform_candidates: PlatformCandidates,\n) -> MergedSourceCandidate | None:\n    return _choose_by_precedence(\n        platform_candidates.conda,\n        platform_candidates.pip,\n    )\n\n\ndef _select_pip_candidate(\n    platform_candidates: PlatformCandidates,\n) -> MergedSourceCandidate | None:\n    if platform_candidates.pip is None:\n        return None\n    return platform_candidates.pip\n\n\ndef _final_identity(candidate: MergedSourceCandidate) -> str:\n    if candidate.source == \"conda\":\n        return candidate.spec.name\n    return candidate.normalized_name\n\n\ndef _merge_candidate_group(\n    candidates: Iterable[MergedSourceCandidate],\n) -> MergedSourceCandidate:\n    ordered = sorted(candidates, key=_candidate_display_key)\n    source = ordered[0].source\n    requirements = [\n        requirement for candidate in ordered for requirement in candidate.requirements\n    ]\n    return _merge_source_requirements(source, requirements)\n\n\ndef _can_reconcile_cross_source_collision(\n    candidates: Iterable[MergedSourceCandidate],\n) -> bool:\n    conda_names = {\n        conda_name\n        for candidate in candidates\n        for conda_name, _pip_name in candidate.family_keys\n        if conda_name is not None\n    }\n    pip_names = {\n        pip_name\n        for candidate in candidates\n        for _conda_name, pip_name in candidate.family_keys\n        if pip_name is not None\n    }\n    return len(conda_names) <= 1 and len(pip_names) <= 1\n\n\ndef _raise_final_collision(\n    *,\n    platform: TargetPlatform,\n    identity: str,\n    candidates: Iterable[MergedSourceCandidate],\n) -> None:\n    platform_text = platform or \"universal\"\n    rendered = \"\\n\".join(\n        f\"  - {_candidate_to_text(candidate)}\"\n        for candidate in sorted(candidates, key=_candidate_display_key)\n    )\n    msg = (\n        \"Final Dependency Collision:\\n\"\n        f\"Multiple selected dependency families map to final install identity \"\n        f\"'{identity}' on platform '{platform_text}':\\n\"\n        f\"{rendered}\\n\"\n        \"Resolve the ambiguity by removing one alternative or making the target \"\n        \"package names distinct.\"\n    )\n    raise ValueError(msg)\n\n\ndef _resolve_final_collisions(\n    selected: dict[TargetPlatform, list[MergedSourceCandidate]],\n) -> dict[TargetPlatform, list[MergedSourceCandidate]]:\n    resolved: dict[TargetPlatform, list[MergedSourceCandidate]] = {}\n    for platform, candidates in selected.items():\n        by_identity: dict[str, list[MergedSourceCandidate]] = {}\n        for candidate in candidates:\n            by_identity.setdefault(_final_identity(candidate), []).append(candidate)\n        resolved_candidates: list[MergedSourceCandidate] = []\n        for identity, group in sorted(by_identity.items()):\n            if len(group) == 1:\n                resolved_candidates.append(group[0])\n                continue\n            by_source: dict[CondaPip, list[MergedSourceCandidate]] = {}\n            for candidate in group:\n                by_source.setdefault(candidate.source, []).append(candidate)\n            merged_group = [\n                _merge_candidate_group(source_candidates)\n                for _source, source_candidates in sorted(by_source.items())\n            ]\n            sources = {candidate.source for candidate in merged_group}\n            if len(sources) > 1 and not _can_reconcile_cross_source_collision(\n                merged_group,\n            ):\n                _raise_final_collision(\n                    platform=platform,\n                    identity=identity,\n                    candidates=merged_group,\n                )\n            if len(sources) > 1:\n                conda = next(\n                    (\n                        candidate\n                        for candidate in merged_group\n                        if candidate.source == \"conda\"\n                    ),\n                    None,\n                )\n                pip = next(\n                    (\n                        candidate\n                        for candidate in merged_group\n                        if candidate.source == \"pip\"\n                    ),\n                    None,\n                )\n                winner = _choose_by_precedence(conda, pip)\n                assert winner is not None\n                resolved_candidates.append(winner)\n                continue\n            resolved_candidates.append(merged_group[0])\n        resolved[platform] = resolved_candidates\n    return resolved\n\n\ndef select_conda_like_requirements(\n    entries: Sequence[DependencyEntry],\n    platforms: Sequence[Platform] | None = None,\n) -> dict[TargetPlatform, list[MergedSourceCandidate]]:\n    selected: dict[TargetPlatform, list[MergedSourceCandidate]] = {}\n    for platform_candidates in _build_platform_candidates(entries, platforms):\n        candidate = _select_conda_like_candidate(platform_candidates)\n        assert candidate is not None\n        selected.setdefault(platform_candidates.platform, []).append(candidate)\n    return _resolve_final_collisions(selected)\n\n\ndef select_pip_requirements(\n    entries: Sequence[DependencyEntry],\n    platforms: Sequence[Platform] | None = None,\n) -> dict[TargetPlatform, list[MergedSourceCandidate]]:\n    selected: dict[TargetPlatform, list[MergedSourceCandidate]] = {}\n    for platform_candidates in _build_platform_candidates(entries, platforms):\n        candidate = _select_pip_candidate(platform_candidates)\n        if candidate is None:\n            continue\n        selected.setdefault(platform_candidates.platform, []).append(candidate)\n    return _resolve_final_collisions(selected)\n\n\ndef collapse_selected_universals(\n    selected: dict[TargetPlatform, list[MergedSourceCandidate]],\n    platforms: Sequence[Platform] | None = None,\n) -> dict[TargetPlatform, list[MergedSourceCandidate]]:\n    \"\"\"Compress identical universal-origin candidates back to the universal bucket.\"\"\"\n    result: dict[TargetPlatform, list[MergedSourceCandidate]] = {}\n\n    active_platforms = (\n        list(platforms)\n        if platforms\n        else sorted(platform for platform in selected if platform is not None)\n    )\n    if not active_platforms:\n        return result\n\n    grouped: dict[\n        tuple[CondaPip, Spec],\n        dict[Platform, MergedSourceCandidate],\n    ] = {}\n    for platform in active_platforms:\n        for candidate in selected.get(platform, []):\n            grouped.setdefault(\n                (candidate.source, candidate.spec),\n                {},\n            )[platform] = candidate\n\n    for candidates_by_platform in grouped.values():\n        if len(candidates_by_platform) == len(active_platforms) and all(\n            _candidate_has_universal_origin(candidate)\n            for candidate in candidates_by_platform.values()\n        ):\n            result.setdefault(None, []).append(\n                next(iter(candidates_by_platform.values())),\n            )\n            continue\n        for platform, candidate in candidates_by_platform.items():\n            result.setdefault(platform, []).append(candidate)\n\n    return result\n"
  },
  {
    "path": "unidep/_hatch_integration.py",
    "content": "\"\"\"unidep - Unified Conda and Pip requirements management.\n\nThis module contains the Hatchling integration.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nfrom hatchling.metadata.plugin.interface import MetadataHookInterface\nfrom hatchling.plugin import hookimpl\n\nfrom unidep._setuptools_integration import _deps\nfrom unidep.utils import (\n    parse_folder_or_filename,\n)\n\n__all__ = [\"UnidepRequirementsMetadataHook\"]\n\n\nclass UnidepRequirementsMetadataHook(MetadataHookInterface):\n    \"\"\"Hatch hook to populate ``'project.depencencies'`` from ``requirements.yaml`` or ``pyproject.toml``.\"\"\"  # noqa: E501\n\n    PLUGIN_NAME = \"unidep\"\n\n    def update(self, metadata: dict) -> None:\n        \"\"\"Update the project table's metadata.\"\"\"\n        if \"dependencies\" not in metadata.get(\"dynamic\", []):\n            return\n        project_root = Path.cwd()\n        try:\n            requirements_file = parse_folder_or_filename(project_root).path\n        except FileNotFoundError:\n            return\n        if \"dependencies\" in metadata:\n            error_msg = (\n                \"You have a `requirements.yaml` file in your project root or\"\n                \" configured unidep in `pyproject.toml` with `[tool.unidep]`,\"\n                \" but you are also using `[project.dependencies]`.\"\n                \" Please remove `[project.dependencies]`, you cannot use both.\"\n            )\n            raise RuntimeError(error_msg)\n\n        deps = _deps(requirements_file)\n        metadata[\"dependencies\"] = deps.dependencies\n        if \"optional-dependencies\" not in metadata.get(\"dynamic\", []):\n            return\n        metadata[\"optional-dependencies\"] = deps.extras\n\n\n@hookimpl\ndef hatch_register_metadata_hook() -> type[UnidepRequirementsMetadataHook]:\n    return UnidepRequirementsMetadataHook\n"
  },
  {
    "path": "unidep/_pixi.py",
    "content": "\"\"\"Pixi.toml generation with version constraint merging.\"\"\"\n\nfrom __future__ import annotations\n\nimport copy\nimport os\nimport re\nimport sys\nfrom collections import Counter, deque\nfrom collections.abc import Mapping\nfrom pathlib import Path\nfrom typing import (\n    TYPE_CHECKING,\n    Any,\n    Literal,\n    NamedTuple,\n    Sequence,\n    cast,\n)\n\nfrom ruamel.yaml import YAML\n\nfrom unidep._dependencies_parsing import (\n    DependencyEntry,\n    _apply_local_dependency_override,\n    _effective_local_dependencies,\n    _load,\n    _move_local_optional_dependencies_to_local_dependencies,\n    _str_is_path_like,\n    _try_parse_local_dependency_requirement_file,\n    parse_requirements,\n)\nfrom unidep._dependency_selection import select_conda_like_requirements\nfrom unidep.platform_definitions import Platform\nfrom unidep.utils import (\n    LocalDependency,\n    PathWithExtras,\n    is_pip_installable,\n    package_name_from_path,\n    parse_folder_or_filename,\n    resolve_platforms,\n    split_path_and_extras,\n)\n\nif TYPE_CHECKING:\n    from typing import Dict, Optional, Tuple, Union\n\n    from unidep._dependencies_parsing import ParsedRequirements\n    from unidep.platform_definitions import Spec\n\n    if sys.version_info >= (3, 10):\n        from typing import TypeAlias\n    else:\n        from typing_extensions import TypeAlias\n\n    from unidep.platform_definitions import Platform\n\n    # Version spec can be a string or dict with version/build/extras\n    VersionSpec: TypeAlias = Union[str, Dict[str, Any]]\n\n    # Type alias for the extracted dependencies structure\n    # Maps platform (or None for universal) to (conda_deps, pip_deps)\n    PlatformDeps: TypeAlias = Dict[\n        Optional[str],\n        Tuple[Dict[str, VersionSpec], Dict[str, VersionSpec]],\n    ]\n\n\ndef _parse_version_build(pin: str | None) -> str | dict[str, str]:\n    \"\"\"Parse a version pin that may contain a build string.\n\n    Conda matchspecs can have format: \">=1.0 build_string*\"\n    where the build string comes after a space following the version.\n\n    Returns:\n        str: Simple version string like \">=1.0\" or \"*\"\n        dict: {\"version\": \">=1.0\", \"build\": \"build_string*\"} when build present\n\n    \"\"\"\n    if not pin:\n        return \"*\"\n\n    pin = pin.strip()\n    if not pin:\n        return \"*\"\n\n    # Build strings come after the full version constraint, separated by whitespace.\n    # We split on the last whitespace and only treat the last token as build when\n    # the version part looks complete (has digits or a wildcard) and the last token\n    # doesn't look like another constraint.\n    if \" \" in pin:\n        version_candidate, build_candidate = pin.rsplit(None, 1)\n        if (\n            re.search(r\"\\d\", version_candidate) or \"*\" in version_candidate\n        ) and not re.match(r\"^[><=!~]\", build_candidate):\n            version = version_candidate.replace(\" \", \"\")\n            return {\"version\": version, \"build\": build_candidate}\n\n    # No build string, just return the version without spaces\n    return pin.replace(\" \", \"\")\n\n\ndef _parse_package_extras(pkg_name: str) -> tuple[str, list[str]]:\n    \"\"\"Parse a package name that may contain extras.\n\n    Pip packages can have format: \"package[extra1,extra2]\"\n\n    Returns:\n        tuple: (base_name, extras_list) where extras_list is empty if no extras\n\n    \"\"\"\n    match = re.match(r\"^([a-zA-Z0-9_.\\-]+)\\[([^\\]]+)\\]$\", pkg_name)\n    if match:\n        base_name = match.group(1)\n        extras = [e.strip() for e in match.group(2).split(\",\")]\n        return base_name, extras\n    return pkg_name, []\n\n\ndef _make_pip_version_spec(\n    version: str | dict[str, str],\n    extras: list[str],\n) -> str | dict[str, Any]:\n    \"\"\"Create a pip version spec, handling extras if present.\n\n    Pixi requires extras in table format:\n        package = { version = \"*\", extras = [\"extra1\", \"extra2\"] }\n\n    Returns:\n        str: Simple version string if no extras\n        dict: Table with version and extras if extras present\n\n    \"\"\"\n    if not extras:\n        return version\n\n    # When we have extras, we need table format\n    if isinstance(version, str):\n        return {\"version\": version, \"extras\": extras}\n    # version is already a dict (has build string), add extras\n    return {**version, \"extras\": extras}\n\n\ndef _get_package_name(project_dir: Path) -> str:\n    \"\"\"Get a pixi dependency key for an editable local package.\"\"\"\n    name = package_name_from_path(project_dir)\n    return name.replace(\"-\", \"_\").replace(\".\", \"_\")\n\n\ndef _normalize_feature_name(name: str) -> str:\n    \"\"\"Normalize a feature name to a deterministic pixi-friendly key.\"\"\"\n    return re.sub(r\"[^A-Za-z0-9_-]+\", \"-\", name.strip()).strip(\"-_\")\n\n\ndef _project_dir_from_requirement_file(req_file: Path) -> Path:\n    \"\"\"Get the installable project directory for a requirements path.\"\"\"\n    resolved = req_file.resolve()\n    return resolved.parent if resolved.is_file() else resolved\n\n\ndef _derive_feature_names(requirements_files: Sequence[Path]) -> list[str]:\n    \"\"\"Derive unique, non-empty feature names for requirements files.\"\"\"\n    project_dirs = [\n        _project_dir_from_requirement_file(req_file) for req_file in requirements_files\n    ]\n    resolved_paths = [req_file.resolve() for req_file in requirements_files]\n\n    base_names = []\n    for req_file, req_path, req_dir in zip(\n        requirements_files,\n        resolved_paths,\n        project_dirs,\n    ):\n        # Prefer the file stem for non-standard requirement filenames\n        # (e.g. dev-requirements.yaml) so shared files get meaningful feature names.\n        if req_path.name not in {\"requirements.yaml\", \"pyproject.toml\"}:\n            default_name = req_path.stem\n        else:\n            default_name = req_dir.name or req_path.stem or req_file.stem or \"feature\"\n        normalized = _normalize_feature_name(default_name)\n        base_names.append(normalized or \"feature\")\n\n    try:\n        common_dir = Path(os.path.commonpath([str(path) for path in project_dirs]))\n    except ValueError:\n        common_dir = Path.cwd().resolve()\n    base_counts = Counter(base_names)\n    used_names: set[str] = set()\n    feature_names: list[str] = []\n\n    for base_name, req_path, req_dir in zip(base_names, resolved_paths, project_dirs):\n        if base_counts[base_name] == 1:\n            candidate = base_name\n        else:\n            try:\n                rel_parts = req_dir.relative_to(common_dir).parts\n            except ValueError:\n                rel_parts = req_dir.parts\n            rel_name = _normalize_feature_name(\n                \"-\".join(part for part in rel_parts if part),\n            )\n            candidate = rel_name or base_name or \"feature\"\n\n            if candidate in used_names:\n                stem_name = _normalize_feature_name(req_path.stem)\n                if stem_name:\n                    candidate = _normalize_feature_name(f\"{candidate}-{stem_name}\")\n\n        unique_name = candidate\n        suffix = 2\n        while unique_name in used_names:\n            unique_name = f\"{candidate}-{suffix}\"\n            suffix += 1\n        used_names.add(unique_name)\n        feature_names.append(unique_name)\n\n    return feature_names\n\n\ndef _editable_dependency_path(req_dir: Path, output_file: str | Path | None) -> str:\n    \"\"\"Build editable path relative to the generated pixi.toml location.\"\"\"\n    output_dir = (\n        Path.cwd().resolve()\n        if output_file is None\n        else Path(output_file).resolve().parent\n    )\n    try:\n        rel_path = Path(os.path.relpath(req_dir.resolve(), output_dir)).as_posix()\n    except ValueError:\n        # On Windows, os.path.relpath raises ValueError when paths are on\n        # different drives (e.g. C:\\ vs D:\\).  Fall back to an absolute path.\n        return req_dir.resolve().as_posix()\n    if rel_path == \".\":\n        return \".\"\n    if rel_path.startswith(\".\"):\n        return rel_path\n    return f\"./{rel_path}\"\n\n\ndef _with_unique_order_paths(items: Sequence[Path]) -> list[Path]:\n    \"\"\"Return unique paths while preserving order.\"\"\"\n    unique_items: list[Path] = []\n    seen: set[Path] = set()\n    for item in items:\n        resolved = item.resolve()\n        if resolved in seen:\n            continue\n        seen.add(resolved)\n        unique_items.append(item)\n    return unique_items\n\n\ndef _add_editable_local_dependencies(\n    section: dict[str, Any],\n    local_projects: Sequence[Path],\n    *,\n    output_file: str | Path | None,\n    exclude: set[Path] | None = None,\n) -> None:\n    \"\"\"Add local projects to a pixi section as editable pip dependencies.\n\n    Parameters\n    ----------\n    section\n        The pixi data dict to add ``pypi-dependencies`` entries to.\n    local_projects\n        Directories of installable Python projects.\n    output_file\n        Path to the generated pixi.toml (used to compute relative paths).\n    exclude\n        Resolved paths to skip (used to avoid duplicating editables that\n        already appear in a parent/base section).\n\n    \"\"\"\n    unique_projects = _with_unique_order_paths(list(local_projects))\n    if not unique_projects:\n        return\n    for project_dir in unique_projects:\n        if exclude and project_dir.resolve() in exclude:\n            continue\n        package_name = _get_package_name(project_dir)\n        section.setdefault(\"pypi-dependencies\", {})[package_name] = {\n            \"path\": _editable_dependency_path(project_dir, output_file),\n            \"editable\": True,\n        }\n\n\ndef _unmanaged_installable_local_project_dir(\n    *,\n    base_dir: Path,\n    local_dependency: str,\n) -> Path | None:\n    \"\"\"Resolve an unmanaged local dependency to an installable project directory.\"\"\"\n    local_path, _extras = split_path_and_extras(local_dependency)\n    abs_local = (base_dir / local_path).resolve()\n    if abs_local.suffix in (\".whl\", \".zip\"):\n        return None\n    if is_pip_installable(abs_local):\n        return abs_local\n    return None\n\n\nclass LocalDependencyGraph(NamedTuple):\n    \"\"\"Result of discovering local dependency relationships.\"\"\"\n\n    roots: list[PathWithExtras]\n    discovered: list[PathWithExtras]\n    graph: dict[PathWithExtras, list[PathWithExtras]]\n    optional_group_graph: dict[PathWithExtras, dict[str, list[PathWithExtras]]]\n    unmanaged_local_graph: dict[PathWithExtras, list[Path]]\n    optional_group_unmanaged_graph: dict[PathWithExtras, dict[str, list[Path]]]\n\n\ndef _discover_local_dependency_graph(  # noqa: PLR0912, C901, PLR0915\n    requirements_files: Sequence[Path],\n) -> LocalDependencyGraph:\n    \"\"\"Discover requirement files reachable via local_dependencies.\n\n    Returns:\n        - Root requirement files (the user-provided inputs).\n        - All discovered requirement files (roots + reachable local deps).\n        - A direct dependency graph between discovered requirement files.\n        - Optional-group local dependency edges for root files.\n        - Direct unmanaged installable local dependencies for each node.\n        - Optional-group unmanaged installable local dependencies for root files.\n\n    \"\"\"\n    yaml = YAML(typ=\"rt\")\n    local_dependency_overrides: dict[Path, LocalDependency] = {}\n\n    roots = [\n        parse_folder_or_filename(req_file).canonicalized()\n        for req_file in requirements_files\n    ]\n    discovered: list[PathWithExtras] = []\n    graph: dict[PathWithExtras, list[PathWithExtras]] = {}\n    optional_group_graph: dict[PathWithExtras, dict[str, list[PathWithExtras]]] = {}\n    unmanaged_local_graph: dict[PathWithExtras, list[Path]] = {}\n    optional_group_unmanaged_graph: dict[PathWithExtras, dict[str, list[Path]]] = {}\n    seen: set[PathWithExtras] = set()\n    roots_set = set(roots)\n    queue = deque(roots)\n\n    while queue:\n        node = queue.popleft()\n        if node in seen:\n            continue\n        seen.add(node)\n        discovered.append(node)\n\n        data = copy.deepcopy(_load(node.path, yaml))\n        _move_local_optional_dependencies_to_local_dependencies(\n            data=data,\n            path_with_extras=node,\n            verbose=False,\n        )\n        effective_local_dependencies = _effective_local_dependencies(\n            data=data,\n            base_dir=node.path.parent,\n            overrides=local_dependency_overrides,\n        )\n\n        if node in roots_set:\n            optional_groups = data.get(\"optional_dependencies\", {})\n            if isinstance(optional_groups, Mapping):\n                for group_name, group_deps in optional_groups.items():\n                    if not isinstance(group_deps, list):\n                        continue\n                    for dep in group_deps:\n                        if isinstance(dep, Mapping) or not _str_is_path_like(dep):\n                            continue\n                        effective_local_dep = _apply_local_dependency_override(\n                            local_dependency=LocalDependency(local=dep),\n                            base_dir=node.path.parent,\n                            overrides=local_dependency_overrides,\n                        )\n                        if effective_local_dep.use != \"local\":\n                            continue\n                        requirements_dep_file = (\n                            _try_parse_local_dependency_requirement_file(\n                                base_dir=node.path.parent,\n                                local_dependency=effective_local_dep.local,\n                            )\n                        )\n                        if requirements_dep_file is None:\n                            unmanaged_local_dir = (\n                                _unmanaged_installable_local_project_dir(\n                                    base_dir=node.path.parent,\n                                    local_dependency=effective_local_dep.local,\n                                )\n                            )\n                            if unmanaged_local_dir is None:\n                                continue\n                            unmanaged_group_edges = (\n                                optional_group_unmanaged_graph.setdefault(\n                                    node,\n                                    {},\n                                ).setdefault(group_name, [])\n                            )\n                            if unmanaged_local_dir not in unmanaged_group_edges:\n                                unmanaged_group_edges.append(unmanaged_local_dir)\n                            continue\n                        child = requirements_dep_file.canonicalized()\n                        group_edges = optional_group_graph.setdefault(\n                            node,\n                            {},\n                        ).setdefault(group_name, [])\n                        if child not in group_edges:\n                            group_edges.append(child)\n                        if child not in seen:\n                            queue.append(child)\n\n        direct_nodes: list[PathWithExtras] = []\n        direct_unmanaged_nodes: list[Path] = []\n        for effective_local_dep in effective_local_dependencies:\n            if effective_local_dep.use != \"local\":\n                continue\n            requirements_dep_file = _try_parse_local_dependency_requirement_file(\n                base_dir=node.path.parent,\n                local_dependency=effective_local_dep.local,\n            )\n            if requirements_dep_file is None:\n                unmanaged_local_dir = _unmanaged_installable_local_project_dir(\n                    base_dir=node.path.parent,\n                    local_dependency=effective_local_dep.local,\n                )\n                if (\n                    unmanaged_local_dir is not None\n                    and unmanaged_local_dir not in direct_unmanaged_nodes\n                ):\n                    direct_unmanaged_nodes.append(unmanaged_local_dir)\n                continue\n            child = requirements_dep_file.canonicalized()\n            if child not in direct_nodes:\n                direct_nodes.append(child)\n            if child not in seen:\n                queue.append(child)\n\n        graph[node] = direct_nodes\n        unmanaged_local_graph[node] = direct_unmanaged_nodes\n\n    return LocalDependencyGraph(\n        roots=roots,\n        discovered=discovered,\n        graph=graph,\n        optional_group_graph=optional_group_graph,\n        unmanaged_local_graph=unmanaged_local_graph,\n        optional_group_unmanaged_graph=optional_group_unmanaged_graph,\n    )\n\n\ndef _parse_direct_requirements_for_node(\n    node: PathWithExtras,\n    *,\n    verbose: bool,\n    ignore_pins: list[str] | None,\n    skip_dependencies: list[str] | None,\n    overwrite_pins: list[str] | None,\n    include_all_optional_groups: bool = False,\n) -> ParsedRequirements:\n    \"\"\"Parse a requirements node without recursively flattening local deps.\"\"\"\n    extras: list[list[str]] | Literal[\"*\"] | None\n    if node.extras:\n        extras = [node.extras]\n    elif include_all_optional_groups:\n        extras = \"*\"\n    else:\n        extras = None\n    req = parse_requirements(\n        node.path,\n        verbose=verbose,\n        extras=extras,\n        ignore_pins=ignore_pins,\n        overwrite_pins=overwrite_pins,\n        skip_dependencies=skip_dependencies,\n        include_local_dependencies=False,\n    )\n\n    if not node.extras:\n        return req\n\n    merged_requirements = {\n        name: list(specs) for name, specs in req.requirements.items()\n    }\n    merged_entries = list(req.dependency_entries)\n    if \"*\" in node.extras:\n        selected_groups = list(req.optional_dependencies.keys())\n    else:\n        selected_groups = [\n            group_name\n            for group_name in node.extras\n            if group_name in req.optional_dependencies\n        ]\n\n    # Extras selected on local dependencies are required for the parent feature.\n    for group_name in selected_groups:\n        for dep_name, specs in req.optional_dependencies[group_name].items():\n            merged_requirements.setdefault(dep_name, []).extend(specs)\n        merged_entries.extend(req.optional_dependency_entries.get(group_name, []))\n\n    return req._replace(\n        requirements=merged_requirements,\n        optional_dependencies={},\n        dependency_entries=merged_entries,\n        optional_dependency_entries={},\n    )\n\n\ndef _collect_transitive_nodes(\n    node: PathWithExtras,\n    graph: dict[PathWithExtras, list[PathWithExtras]],\n) -> list[PathWithExtras]:\n    \"\"\"Collect transitive local dependency nodes in deterministic order.\"\"\"\n    collected: list[PathWithExtras] = []\n    seen: set[PathWithExtras] = set()\n    queue = deque(graph.get(node, []))\n\n    while queue:\n        current = queue.popleft()\n        if current in seen:\n            continue\n        seen.add(current)\n        collected.append(current)\n        queue.extend(graph.get(current, []))\n\n    return collected\n\n\ndef _with_unique_order(items: list[str]) -> list[str]:\n    \"\"\"Return unique items while preserving order.\"\"\"\n    return list(dict.fromkeys(items))\n\n\ndef _unique_optional_feature_name(\n    *,\n    parent_feature: str,\n    group_name: str,\n    taken_names: set[str],\n) -> str:\n    \"\"\"Generate a non-colliding optional sub-feature name.\"\"\"\n    candidate = f\"{parent_feature}-{group_name}\"\n    if candidate not in taken_names:\n        taken_names.add(candidate)\n        return candidate\n\n    suffix_base = f\"{candidate}-opt\"\n    unique_candidate = suffix_base\n    suffix = 2\n    while unique_candidate in taken_names:\n        unique_candidate = f\"{suffix_base}-{suffix}\"\n        suffix += 1\n    taken_names.add(unique_candidate)\n    return unique_candidate\n\n\ndef _unique_env_name(\n    feature_name: str,\n    taken_env_names: set[str],\n) -> str:\n    \"\"\"Generate a non-colliding pixi environment name from a feature name.\n\n    Pixi environment names cannot contain underscores, so we replace them\n    with hyphens.  When this normalization causes a collision (e.g. both\n    ``foo_bar`` and ``foo-bar`` exist), a numeric suffix is appended.\n    \"\"\"\n    candidate = feature_name.replace(\"_\", \"-\")\n    if candidate not in taken_env_names:\n        taken_env_names.add(candidate)\n        return candidate\n\n    suffix = 2\n    while f\"{candidate}-{suffix}\" in taken_env_names:\n        suffix += 1\n    result = f\"{candidate}-{suffix}\"\n    taken_env_names.add(result)\n    return result\n\n\ndef _add_single_file_optional_environments(\n    pixi_data: dict[str, Any],\n    opt_features: list[str],\n) -> None:\n    \"\"\"Add single-file optional environments, avoiding `all` name collisions.\"\"\"\n    if not opt_features:\n        return\n\n    pixi_data[\"environments\"][\"default\"] = []\n    create_aggregate_all_env = len(opt_features) > 1\n    taken_env_names: set[str] = {\"default\"} | (\n        {\"all\"} if create_aggregate_all_env else set()\n    )\n\n    for feat in opt_features:\n        env_name = _unique_env_name(feat, taken_env_names)\n        pixi_data[\"environments\"][env_name] = [feat]\n\n    if create_aggregate_all_env:\n        pixi_data[\"environments\"][\"all\"] = opt_features\n\n\ndef _spec_key(spec: Spec) -> tuple[str, str, str | None, str | None]:\n    \"\"\"Return the stable identity of a Spec (excludes parse-time identifier).\"\"\"\n    return (spec.name, spec.which, spec.pin, spec.selector)\n\n\ndef _entry_key(\n    entry: DependencyEntry,\n) -> tuple[\n    tuple[str, str, str | None, str | None] | None,\n    tuple[str, str, str | None, str | None] | None,\n]:\n    \"\"\"Return the stable identity of a dependency entry.\"\"\"\n    conda = _spec_key(entry.conda) if entry.conda is not None else None\n    pip = _spec_key(entry.pip) if entry.pip is not None else None\n    return (conda, pip)\n\n\ndef _subtract_entries(\n    full_entries: list[DependencyEntry],\n    base_entries: list[DependencyEntry],\n) -> list[DependencyEntry]:\n    \"\"\"Return entries present in full_entries but not in base_entries.\"\"\"\n    remaining = Counter(_entry_key(entry) for entry in base_entries)\n    diff: list[DependencyEntry] = []\n    for entry in full_entries:\n        key = _entry_key(entry)\n        if remaining[key] > 0:\n            remaining[key] -= 1\n        else:\n            diff.append(entry)\n    return diff\n\n\nclass _PixiGenerationResult(NamedTuple):\n    \"\"\"Intermediate result from single-file or multi-file pixi generation.\"\"\"\n\n    pixi_data: dict[str, Any]\n    all_channels: set[str]\n    all_platforms: set[str]\n    discovered_target_platforms: set[str]\n\n\ndef _process_single_file_optional_groups(\n    pixi_data: dict[str, Any],\n    *,\n    req_file: Path,\n    base_req: ParsedRequirements,\n    base_feature_platforms: list[Platform] | None,\n    dep_graph: LocalDependencyGraph,\n    root_node: PathWithExtras,\n    base_local_editable_set: set[Path],\n    output_file: str | Path | None,\n    verbose: bool,\n    ignore_pins: list[str] | None,\n    skip_dependencies: list[str] | None,\n    overwrite_pins: list[str] | None,\n) -> set[str]:\n    \"\"\"Process optional dependency groups for single-file pixi generation.\n\n    Returns discovered target platforms.\n    \"\"\"\n    discovered_target_platforms: set[str] = set()\n\n    optional_data = _load(req_file, YAML(typ=\"rt\")).get(\"optional_dependencies\", {})\n    optional_groups = list(optional_data) if isinstance(optional_data, Mapping) else []\n    if not optional_groups:\n        return discovered_target_platforms\n\n    pixi_data[\"feature\"] = {}\n    pixi_data[\"environments\"] = {}\n    opt_features = []\n    workspace_platforms: set[Platform] = set(base_feature_platforms or [])\n    parsed_groups: list[tuple[str, list[DependencyEntry]]] = []\n\n    for group_name in optional_groups:\n        group_req = parse_requirements(\n            req_file,\n            verbose=verbose,\n            extras=[[group_name]],\n            ignore_pins=ignore_pins,\n            overwrite_pins=overwrite_pins,\n            skip_dependencies=skip_dependencies,\n            include_local_dependencies=True,\n        )\n        # A group parse contains the base requirements plus group-selected\n        # optional local dependencies. Keep only the delta to preserve\n        # optional semantics.\n        group_feature_entries = _subtract_entries(\n            group_req.dependency_entries,\n            base_req.dependency_entries,\n        )\n        group_feature_entries.extend(\n            group_req.optional_dependency_entries.get(group_name, []),\n        )\n        workspace_platforms.update(\n            _selector_platforms_from_entries(group_feature_entries),\n        )\n        parsed_groups.append((group_name, group_feature_entries))\n\n    group_platforms = sorted(workspace_platforms) or None\n    if group_platforms:\n        discovered_target_platforms.update(group_platforms)\n\n    for group_name, group_feature_entries in parsed_groups:\n        opt_platform_deps = _extract_dependencies(\n            group_feature_entries,\n            platforms=group_platforms,\n            allow_hoist_without_universal_origin=True,\n        )\n        feature = _build_feature_dict(opt_platform_deps)\n        optional_group_projects: list[Path] = list(\n            dep_graph.optional_group_unmanaged_graph.get(root_node, {}).get(\n                group_name,\n                [],\n            ),\n        )\n        optional_local_nodes = dep_graph.optional_group_graph.get(\n            root_node,\n            {},\n        ).get(\n            group_name,\n            [],\n        )\n        seen_optional_nodes: set[PathWithExtras] = set()\n        for optional_local_node in optional_local_nodes:\n            for candidate_node in [\n                optional_local_node,\n                *(\n                    _collect_transitive_nodes(\n                        optional_local_node,\n                        dep_graph.graph,\n                    )\n                ),\n            ]:\n                if candidate_node in seen_optional_nodes:\n                    continue\n                seen_optional_nodes.add(candidate_node)\n                optional_project_dir = _project_dir_from_requirement_file(\n                    candidate_node.path,\n                )\n                if is_pip_installable(optional_project_dir):\n                    optional_group_projects.append(optional_project_dir)\n                optional_group_projects.extend(\n                    dep_graph.unmanaged_local_graph.get(candidate_node, []),\n                )\n        _add_editable_local_dependencies(\n            feature,\n            optional_group_projects,\n            output_file=output_file,\n            exclude=base_local_editable_set,\n        )\n        if feature:\n            pixi_data[\"feature\"][group_name] = feature\n            opt_features.append(group_name)\n\n    # Create environments for optional dependencies\n    _add_single_file_optional_environments(pixi_data, opt_features)\n\n    return discovered_target_platforms\n\n\ndef _generate_single_file_pixi(\n    requirements_file: Path,\n    *,\n    platforms_override: list[Platform] | None,\n    output_file: str | Path | None,\n    verbose: bool,\n    ignore_pins: list[str] | None,\n    skip_dependencies: list[str] | None,\n    overwrite_pins: list[str] | None,\n) -> _PixiGenerationResult:\n    \"\"\"Generate pixi data for a single requirements file.\"\"\"\n    pixi_data: dict[str, Any] = {}\n    all_channels: set[str] = set()\n    all_platforms: set[str] = set()\n    discovered_target_platforms: set[str] = set()\n\n    req_file = parse_folder_or_filename(requirements_file).path\n    base_req = parse_requirements(\n        requirements_file,\n        verbose=verbose,\n        ignore_pins=ignore_pins,\n        overwrite_pins=overwrite_pins,\n        skip_dependencies=skip_dependencies,\n        include_local_dependencies=True,\n    )\n    base_feature_platforms = _feature_platforms_for_entries(\n        entries=base_req.dependency_entries,\n        declared_platforms=base_req.platforms,\n        global_declared_platforms=set(base_req.platforms),\n        platforms_override=platforms_override,\n    )\n    platform_deps = _extract_dependencies(\n        base_req.dependency_entries,\n        platforms=base_feature_platforms,\n        allow_hoist_without_universal_origin=True,\n    )\n    if base_feature_platforms:\n        discovered_target_platforms.update(base_feature_platforms)\n\n    # Use channels and platforms from the requirements file\n    if base_req.channels:\n        all_channels.update(base_req.channels)\n    if base_req.platforms and not platforms_override:\n        all_platforms.update(base_req.platforms)\n\n    pixi_data.update(_build_feature_dict(platform_deps))\n\n    dep_graph = _discover_local_dependency_graph([requirements_file])\n    root_node = dep_graph.roots[0]\n\n    # Collect editable packages from the root project and required local deps\n    # only (NOT optional-only local deps, which belong in optional features).\n    required_nodes = set(_collect_transitive_nodes(root_node, dep_graph.graph))\n    req_dir = _project_dir_from_requirement_file(req_file)\n    local_editable_projects: list[Path] = []\n    if is_pip_installable(req_dir):\n        local_editable_projects.append(req_dir)\n    for node in dep_graph.discovered:\n        if node == root_node or node not in required_nodes:\n            continue\n        node_project_dir = _project_dir_from_requirement_file(node.path)\n        should_add_editable = node.path.name in {\n            \"requirements.yaml\",\n            \"pyproject.toml\",\n        }\n        if should_add_editable and is_pip_installable(node_project_dir):\n            local_editable_projects.append(node_project_dir)\n        local_editable_projects.extend(dep_graph.unmanaged_local_graph.get(node, []))\n    local_editable_projects.extend(dep_graph.unmanaged_local_graph.get(root_node, []))\n    _add_editable_local_dependencies(\n        pixi_data,\n        local_editable_projects,\n        output_file=output_file,\n    )\n    base_local_editable_set = {\n        path.resolve() for path in _with_unique_order_paths(local_editable_projects)\n    }\n\n    # Handle optional dependencies as features\n    opt_target_platforms = _process_single_file_optional_groups(\n        pixi_data,\n        req_file=req_file,\n        base_req=base_req,\n        base_feature_platforms=base_feature_platforms,\n        dep_graph=dep_graph,\n        root_node=root_node,\n        base_local_editable_set=base_local_editable_set,\n        output_file=output_file,\n        verbose=verbose,\n        ignore_pins=ignore_pins,\n        skip_dependencies=skip_dependencies,\n        overwrite_pins=overwrite_pins,\n    )\n    discovered_target_platforms.update(opt_target_platforms)\n\n    return _PixiGenerationResult(\n        pixi_data=pixi_data,\n        all_channels=all_channels,\n        all_platforms=all_platforms,\n        discovered_target_platforms=discovered_target_platforms,\n    )\n\n\ndef _generate_multi_file_pixi(  # noqa: PLR0912, C901, PLR0915\n    requirements_files: Sequence[Path],\n    *,\n    platforms_override: list[Platform] | None,\n    output_file: str | Path | None,\n    verbose: bool,\n    ignore_pins: list[str] | None,\n    skip_dependencies: list[str] | None,\n    overwrite_pins: list[str] | None,\n) -> _PixiGenerationResult:\n    \"\"\"Generate pixi data for multiple requirements files.\"\"\"\n    pixi_data: dict[str, Any] = {\"feature\": {}, \"environments\": {}}\n    all_channels: set[str] = set()\n    all_platforms: set[str] = set()\n    discovered_target_platforms: set[str] = set()\n    dep_graph = _discover_local_dependency_graph(requirements_files)\n    feature_names = _derive_feature_names(\n        [node.path for node in dep_graph.discovered],\n    )\n    feature_name_by_node = dict(zip(dep_graph.discovered, feature_names))\n    taken_optional_feature_names: set[str] = set(feature_names)\n    root_nodes_set = set(dep_graph.roots)\n    parsed_by_node: dict[PathWithExtras, ParsedRequirements] = {}\n    global_declared_platforms: set[Platform] = set()\n    base_feature_nodes: dict[str, PathWithExtras] = {}\n    optional_feature_parents: dict[str, str] = {}\n    optional_feature_has_feature: dict[str, bool] = {}\n    optional_feature_local_nodes: dict[str, list[PathWithExtras]] = {}\n\n    for node in dep_graph.discovered:\n        req = _parse_direct_requirements_for_node(\n            node,\n            verbose=verbose,\n            ignore_pins=ignore_pins,\n            skip_dependencies=skip_dependencies,\n            overwrite_pins=overwrite_pins,\n            include_all_optional_groups=node in root_nodes_set,\n        )\n        parsed_by_node[node] = req\n        if req.platforms and not platforms_override:\n            global_declared_platforms.update(req.platforms)\n\n    for node in dep_graph.discovered:\n        req = parsed_by_node[node]\n        feature_platforms = _feature_platforms_for_entries(\n            entries=req.dependency_entries,\n            declared_platforms=req.platforms,\n            global_declared_platforms=global_declared_platforms,\n            platforms_override=platforms_override,\n        )\n        platform_deps = _extract_dependencies(\n            req.dependency_entries,\n            platforms=feature_platforms,\n            allow_hoist_without_universal_origin=platforms_override is not None\n            or not req.platforms,\n        )\n        discovered_target_platforms.update(\n            platform for platform in platform_deps if platform is not None\n        )\n        feature_name = feature_name_by_node[node]\n\n        # Collect channels and platforms\n        if req.channels:\n            all_channels.update(req.channels)\n        if not platforms_override and feature_platforms:\n            all_platforms.update(feature_platforms)\n\n        # Build the feature dict from platform deps\n        feature = _build_feature_dict(platform_deps)\n\n        # Add editable dependency for standard project requirement files.\n        req_dir = _project_dir_from_requirement_file(node.path)\n        should_add_editable = node.path.name in {\n            \"requirements.yaml\",\n            \"pyproject.toml\",\n        }\n        node_editable_projects: list[Path] = []\n        if should_add_editable and is_pip_installable(req_dir):\n            node_editable_projects.append(req_dir)\n        node_editable_projects.extend(dep_graph.unmanaged_local_graph.get(node, []))\n        _add_editable_local_dependencies(\n            feature,\n            node_editable_projects,\n            output_file=output_file,\n        )\n\n        if feature:  # Only add non-empty features\n            pixi_data[\"feature\"][feature_name] = feature\n        # Always track the node so transitive deps are computed even when\n        # the root itself has no direct dependencies (aggregator pattern).\n        base_feature_nodes[feature_name] = node\n\n        if node not in root_nodes_set:\n            continue\n\n        # Build set of editables already in the base feature so optional\n        # sub-features don't duplicate them (mirrors single-file behavior).\n        base_editable_set = {\n            p.resolve() for p in _with_unique_order_paths(node_editable_projects)\n        }\n\n        # Handle optional dependencies as sub-features for root features.\n        # Even when a root has no direct deps/editables (so no base feature),\n        # its optional groups may still carry real dependencies and must be kept.\n        parsed_group_names = list(req.optional_dependencies)\n        local_only_group_names = set(\n            dep_graph.optional_group_graph.get(node, {}),\n        ) | set(\n            dep_graph.optional_group_unmanaged_graph.get(node, {}),\n        )\n        all_group_names = parsed_group_names + [\n            group_name\n            for group_name in sorted(local_only_group_names)\n            if group_name not in req.optional_dependencies\n        ]\n        for group_name in all_group_names:\n            group_entries = req.optional_dependency_entries.get(group_name, [])\n            group_platforms = _feature_platforms_for_entries(\n                entries=group_entries,\n                declared_platforms=req.platforms,\n                global_declared_platforms=global_declared_platforms,\n                platforms_override=platforms_override,\n            )\n            group_platform_deps = _extract_dependencies(\n                group_entries,\n                platforms=group_platforms,\n                allow_hoist_without_universal_origin=platforms_override is not None\n                or not req.platforms,\n            )\n            discovered_target_platforms.update(\n                platform for platform in group_platform_deps if platform is not None\n            )\n            if not platforms_override and group_platforms:\n                all_platforms.update(group_platforms)\n            opt_feature = _build_feature_dict(group_platform_deps)\n            opt_feature_name = _unique_optional_feature_name(\n                parent_feature=feature_name,\n                group_name=group_name,\n                taken_names=taken_optional_feature_names,\n            )\n            optional_local_nodes = dep_graph.optional_group_graph.get(\n                node,\n                {},\n            ).get(\n                group_name,\n                [],\n            )\n            optional_unmanaged_local_projects = (\n                dep_graph.optional_group_unmanaged_graph.get(\n                    node,\n                    {},\n                ).get(\n                    group_name,\n                    [],\n                )\n            )\n            _add_editable_local_dependencies(\n                opt_feature,\n                optional_unmanaged_local_projects,\n                output_file=output_file,\n                exclude=base_editable_set,\n            )\n            if (\n                not opt_feature\n                and not optional_local_nodes\n                and not optional_unmanaged_local_projects\n            ):\n                continue\n            if opt_feature:\n                pixi_data[\"feature\"][opt_feature_name] = opt_feature\n                optional_feature_has_feature[opt_feature_name] = True\n            else:\n                optional_feature_has_feature[opt_feature_name] = False\n            optional_feature_parents[opt_feature_name] = feature_name\n            optional_feature_local_nodes[opt_feature_name] = optional_local_nodes\n\n    # Create environments\n    if pixi_data[\"feature\"]:\n        transitive_features: dict[str, list[str]] = {}\n        for feature_name, node in base_feature_nodes.items():\n            dep_features = [\n                feature_name_by_node[dep_node]\n                for dep_node in _collect_transitive_nodes(\n                    node,\n                    dep_graph.graph,\n                )\n                if feature_name_by_node.get(dep_node) in pixi_data[\"feature\"]\n            ]\n            transitive_features[feature_name] = _with_unique_order(dep_features)\n\n        default_features: list[str] = []\n        for root_node in dep_graph.roots:\n            root_feature = feature_name_by_node[root_node]\n            # Include the root's own feature only if it's non-empty.\n            if root_feature in pixi_data[\"feature\"]:\n                default_features.append(root_feature)\n            # Always include transitive deps (supports aggregator roots\n            # that have no direct deps but pull in local_dependencies).\n            default_features.extend(transitive_features.get(root_feature, []))\n        pixi_data[\"environments\"][\"default\"] = _with_unique_order(default_features)\n\n        taken_env_names: set[str] = {\"default\"}\n        for opt_feature_name, parent_feature in optional_feature_parents.items():\n            env_name = _unique_env_name(opt_feature_name, taken_env_names)\n            env_features = []\n            if parent_feature in pixi_data[\"feature\"]:\n                env_features.append(parent_feature)\n            env_features.extend(transitive_features.get(parent_feature, []))\n            if optional_feature_has_feature.get(opt_feature_name, False):\n                env_features.append(opt_feature_name)\n            for local_node in optional_feature_local_nodes.get(\n                opt_feature_name,\n                [],\n            ):\n                local_feature = feature_name_by_node[local_node]\n                # Include the local node's own feature if it's non-empty.\n                if local_feature in pixi_data[\"feature\"]:\n                    env_features.append(local_feature)\n                # Always traverse transitive deps even when the local node\n                # itself is empty (aggregator pattern).\n                env_features.extend(transitive_features.get(local_feature, []))\n            pixi_data[\"environments\"][env_name] = _with_unique_order(env_features)\n\n    return _PixiGenerationResult(\n        pixi_data=pixi_data,\n        all_channels=all_channels,\n        all_platforms=all_platforms,\n        discovered_target_platforms=discovered_target_platforms,\n    )\n\n\ndef _selector_platforms_from_entries(\n    entries: Sequence[DependencyEntry],\n) -> list[Platform]:\n    selector_platforms: set[Platform] = set()\n    for entry in entries:\n        for spec in (entry.conda, entry.pip):\n            if spec is None or spec.selector is None:\n                continue\n            entry_platforms = spec.platforms()\n            if entry_platforms is not None:\n                selector_platforms.update(entry_platforms)\n    return sorted(selector_platforms)\n\n\ndef _feature_platforms_for_entries(\n    *,\n    entries: Sequence[DependencyEntry],\n    declared_platforms: Sequence[Platform],\n    global_declared_platforms: set[Platform],\n    platforms_override: list[Platform] | None,\n) -> list[Platform] | None:\n    if platforms_override:\n        return list(platforms_override)\n    if declared_platforms:\n        return list(declared_platforms)\n    inferred_platforms = set(global_declared_platforms)\n    inferred_platforms.update(_selector_platforms_from_entries(entries))\n    return sorted(inferred_platforms) or None\n\n\ndef generate_pixi_toml(\n    *requirements_files: Path,\n    project_name: str | None = None,\n    channels: list[str] | None = None,\n    platforms: list[Platform] | None = None,\n    output_file: str | Path | None = \"pixi.toml\",\n    verbose: bool = False,\n    ignore_pins: list[str] | None = None,\n    skip_dependencies: list[str] | None = None,\n    overwrite_pins: list[str] | None = None,\n) -> None:\n    \"\"\"Generate a pixi.toml file from requirements files.\n\n    This function creates a pixi.toml with features for each requirements file,\n    letting Pixi handle all dependency resolution and conflict management.\n\n    Parameters\n    ----------\n    requirements_files\n        One or more requirement file paths to process.\n    project_name\n        Name for the ``[workspace]`` section. Defaults to the current\n        directory name.\n    channels\n        Conda channels for the workspace.  When provided, these **override**\n        any channels declared in the requirement files (consistent with how\n        *platforms* behaves).  When ``None``, channels are read from the\n        requirement files, falling back to ``[\"conda-forge\"]``.\n    platforms\n        Target platforms.  When provided, overrides file-declared platforms.\n    output_file\n        Path to write the generated TOML.  ``None`` writes to stdout.\n    verbose\n        Print progress information.\n    ignore_pins\n        Package names whose version pins should be stripped.\n    skip_dependencies\n        Package names to omit entirely.\n    overwrite_pins\n        Pin overrides in ``\"pkg>=version\"`` format.\n\n    \"\"\"\n    if not requirements_files:\n        requirements_files = (Path.cwd(),)\n    if platforms is not None and not platforms:\n        platforms = None\n    if len(requirements_files) == 1:\n        result = _generate_single_file_pixi(\n            requirements_files[0],\n            platforms_override=platforms,\n            output_file=output_file,\n            verbose=verbose,\n            ignore_pins=ignore_pins,\n            skip_dependencies=skip_dependencies,\n            overwrite_pins=overwrite_pins,\n        )\n    else:\n        result = _generate_multi_file_pixi(\n            requirements_files,\n            platforms_override=platforms,\n            output_file=output_file,\n            verbose=verbose,\n            ignore_pins=ignore_pins,\n            skip_dependencies=skip_dependencies,\n            overwrite_pins=overwrite_pins,\n        )\n\n    pixi_data = result.pixi_data\n\n    # Set workspace metadata with collected channels and platforms\n    # Sort for deterministic output\n    final_platforms = resolve_platforms(\n        requested_platforms=platforms,\n        declared_platforms=cast(\"set[Platform]\", result.all_platforms),\n        selector_platforms=cast(\"set[Platform]\", result.discovered_target_platforms),\n    )\n    if channels is not None:\n        final_channels = list(channels)\n    elif result.all_channels:\n        final_channels = sorted(result.all_channels)\n    else:\n        final_channels = [\"conda-forge\"]\n    pixi_data[\"workspace\"] = {\n        \"name\": project_name or Path.cwd().name,\n        \"channels\": final_channels,\n        \"platforms\": final_platforms,\n    }\n\n    # Filter target sections to only include platforms in the project's platforms list\n    _filter_targets_by_platforms(pixi_data, set(final_platforms))\n\n    # Write the pixi.toml file\n    _write_pixi_toml(pixi_data, output_file, verbose=verbose)\n\n\ndef _extract_dependencies(  # noqa: PLR0912\n    entries: list[DependencyEntry],\n    *,\n    platforms: list[Platform] | None = None,\n    allow_hoist_without_universal_origin: bool = False,\n) -> PlatformDeps:\n    \"\"\"Extract conda and pip dependencies from dependency entries.\n\n    Returns a dict mapping platform (or None for universal) to\n    ``(conda_deps, pip_deps)``.\n    \"\"\"\n    platform_deps: PlatformDeps = {None: ({}, {})}\n    selected = select_conda_like_requirements(entries, platforms)\n    target_platforms = platforms or sorted(\n        platform for platform in selected if platform is not None\n    )\n\n    if target_platforms:\n        per_platform: dict[\n            Platform,\n            tuple[\n                dict[str, VersionSpec],\n                dict[str, VersionSpec],\n            ],\n        ] = {platform: ({}, {}) for platform in target_platforms}\n        for platform, candidates in selected.items():\n            assert platform is not None\n            conda_deps, pip_deps = per_platform[platform]\n            for candidate in candidates:\n                if candidate.source == \"conda\":\n                    conda_deps[candidate.spec.name] = _parse_version_build(\n                        candidate.spec.pin,\n                    )\n                else:\n                    base_name, extras = _parse_package_extras(candidate.spec.name)\n                    normalized = candidate.spec.name_with_pin(is_pip=True)\n                    normalized_pin = (\n                        normalized[len(candidate.spec.name) :].strip() or None\n                    )\n                    version = _parse_version_build(normalized_pin)\n                    pip_deps[base_name] = _make_pip_version_spec(version, extras)\n\n        universal_conda, universal_pip = platform_deps[None]\n        conda_names = {\n            name\n            for conda_deps, _pip_deps in per_platform.values()\n            for name in conda_deps\n        }\n        pip_names = {\n            name for _conda_deps, pip_deps in per_platform.values() for name in pip_deps\n        }\n\n        for name in sorted(conda_names):\n            present = {\n                platform: deps[0][name]\n                for platform, deps in per_platform.items()\n                if name in deps[0]\n            }\n            if len(present) == len(target_platforms):\n                first_spec = next(iter(present.values()))\n                specs_match = all(spec == first_spec for spec in present.values())\n                hoist_is_safe = allow_hoist_without_universal_origin\n                if specs_match and hoist_is_safe:\n                    universal_conda[name] = first_spec\n                    continue\n            for platform, spec in present.items():\n                platform_deps.setdefault(platform, ({}, {}))[0][name] = spec\n\n        for name in sorted(pip_names):\n            present = {\n                platform: deps[1][name]\n                for platform, deps in per_platform.items()\n                if name in deps[1]\n            }\n            if len(present) == len(target_platforms):\n                first_spec = next(iter(present.values()))\n                specs_match = all(spec == first_spec for spec in present.values())\n                hoist_is_safe = allow_hoist_without_universal_origin\n                if specs_match and hoist_is_safe:\n                    universal_pip[name] = first_spec\n                    continue\n            for platform, spec in present.items():\n                platform_deps.setdefault(platform, ({}, {}))[1][name] = spec\n\n    return platform_deps\n\n\ndef _build_feature_dict(platform_deps: PlatformDeps) -> dict[str, Any]:\n    \"\"\"Build a pixi feature dict from platform dependencies.\"\"\"\n    feature: dict[str, Any] = {}\n\n    # Get universal (non-platform-specific) dependencies\n    conda_deps, pip_deps = platform_deps.get(None, ({}, {}))\n    if conda_deps:\n        feature[\"dependencies\"] = conda_deps\n    if pip_deps:\n        feature[\"pypi-dependencies\"] = pip_deps\n\n    # Add platform-specific dependencies as target sections\n    for platform, (plat_conda, plat_pip) in platform_deps.items():\n        if platform is None:\n            continue\n        if \"target\" not in feature:\n            feature[\"target\"] = {}\n        if platform not in feature[\"target\"]:\n            feature[\"target\"][platform] = {}\n        if plat_conda:\n            feature[\"target\"][platform][\"dependencies\"] = plat_conda\n        if plat_pip:\n            feature[\"target\"][platform][\"pypi-dependencies\"] = plat_pip\n\n    return feature\n\n\ndef _filter_section_targets(\n    section: dict[str, Any],\n    valid_platforms: set[str],\n) -> None:\n    \"\"\"Remove target entries for platforms not in *valid_platforms*.\"\"\"\n    if \"target\" not in section:\n        return\n    section[\"target\"] = {\n        platform: deps\n        for platform, deps in section[\"target\"].items()\n        if platform in valid_platforms\n    }\n    if not section[\"target\"]:\n        del section[\"target\"]\n\n\ndef _filter_targets_by_platforms(\n    pixi_data: dict[str, Any],\n    valid_platforms: set[str],\n) -> None:\n    \"\"\"Filter target sections to only include platforms in valid_platforms.\n\n    This removes targets for platforms that aren't in the project's platforms list,\n    which would otherwise cause pixi to emit warnings.\n    \"\"\"\n    _filter_section_targets(pixi_data, valid_platforms)\n    for feature_data in pixi_data.get(\"feature\", {}).values():\n        _filter_section_targets(feature_data, valid_platforms)\n\n\ndef _write_pixi_toml(\n    pixi_data: dict[str, Any],\n    output_file: str | Path | None,\n    *,\n    verbose: bool = False,\n) -> None:\n    \"\"\"Write the pixi data structure to a TOML file.\"\"\"\n    try:\n        import tomli_w\n    except ImportError:  # pragma: no cover\n        msg = (\n            \"❌ `tomli_w` is required to write TOML files. \"\n            \"Install it with `pip install tomli_w`.\"\n        )\n        raise ImportError(msg) from None\n\n    if output_file is not None:\n        output_path = Path(output_file)\n        with output_path.open(\"wb\") as f:\n            tomli_w.dump(pixi_data, f)\n        if verbose:\n            print(f\"✅ Generated pixi.toml at {output_path}\")\n    else:\n        # Output to stdout\n        tomli_w.dump(pixi_data, sys.stdout.buffer)\n"
  },
  {
    "path": "unidep/_pytest_plugin.py",
    "content": "\"\"\"unidep - Unified Conda and Pip requirements management.\n\nPytest plugin for running only tests of changed files.\n\nWARNING: Still experimental and not documented.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport os\nimport sys\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nfrom unidep._dependencies_parsing import (\n    find_requirements_files,\n    parse_local_dependencies,\n)\n\nif TYPE_CHECKING:\n    import pytest\n\nLOGGER = logging.getLogger(__name__)\n\n\ndef pytest_addoption(parser: pytest.Parser) -> None:  # pragma: no cover\n    \"\"\"Add options to the pytest command line.\"\"\"\n    parser.addoption(\n        \"--run-affected\",\n        action=\"store_true\",\n        default=False,\n        help=\"Run only tests from affected packages (via `unidep`)\",\n    )\n    parser.addoption(\n        \"--branch\",\n        action=\"store\",\n        default=\"origin/main\",\n        help=\"Branch to compare with for finding affected tests\",\n    )\n    parser.addoption(\n        \"--repo-root\",\n        action=\"store\",\n        default=\".\",\n        type=Path,\n        help=\"Root of the repository\",\n    )\n\n\ndef pytest_collection_modifyitems(\n    config: pytest.Config,\n    items: list[pytest.Item],\n) -> None:  # pragma: no cover\n    \"\"\"Filter tests based on the --run-affected option.\"\"\"\n    if not config.getoption(\"--run-affected\"):\n        return\n    try:\n        from git import Repo\n    except ImportError:\n        print(\n            \"🛑 You need to install `gitpython` to use the `--run-affected` option.\"\n            \"run `pip install gitpython` to install it.\",\n        )\n        sys.exit(1)\n\n    compare_branch = config.getoption(\"--branch\")\n    repo_root = Path(config.getoption(\"--repo-root\")).absolute()\n    repo = Repo(repo_root, search_parent_directories=True)\n    repo_root = Path(repo.working_tree_dir)  # In case we searched parent directories\n    found_files = find_requirements_files(repo_root)\n    local_dependencies = parse_local_dependencies(*found_files)\n    staged_diffs = repo.head.commit.diff(compare_branch)\n    unstaged_diffs = repo.index.diff(None)\n    diffs = staged_diffs + unstaged_diffs\n    changed_files = [Path(diff.a_path) for diff in diffs]\n    affected_packages = _affected_packages(repo_root, changed_files, local_dependencies)\n    test_files = [config.cwd_relative_nodeid(i.nodeid).split(\"::\", 1)[0] for i in items]\n    run_from_dir = config.invocation_params.dir\n    assert all((run_from_dir / item).exists() for item in test_files)\n    affected_tests = {\n        item\n        for item, f in zip(items, test_files)\n        if any(f.startswith(str(pkg)) for pkg in affected_packages)\n    }\n    # Run `pytest -o log_cli=true -o log_cli_level=INFO --run-affected`\n    # to see the logging output.\n    logging.info(\n        \"Running affected_tests: %s, changed_files: %s, affected_packages: %s\",\n        affected_tests,\n        changed_files,\n        affected_packages,\n    )\n    items[:] = list(affected_tests)\n\n\ndef _file_in_folder(file: Path, folder: Path) -> bool:  # pragma: no cover\n    file = file.absolute()\n    folder = folder.absolute()\n    common = os.path.commonpath([folder, file])\n    return os.path.commonpath([folder]) == common\n\n\ndef _affected_packages(\n    repo_root: Path,\n    changed_files: list[Path],\n    dependencies: dict[Path, list[Path]],\n    *,\n    verbose: bool = False,\n) -> set[Path]:  # pragma: no cover\n    affected_packages = set()\n    for file in changed_files:\n        for package, deps in dependencies.items():\n            if _file_in_folder(repo_root / file, package):\n                if verbose:\n                    print(f\"File {file} affects package {package}\")\n                affected_packages.add(package)\n                affected_packages.update(deps)\n    return {pkg.relative_to(repo_root) for pkg in affected_packages}\n"
  },
  {
    "path": "unidep/_setuptools_integration.py",
    "content": "#!/usr/bin/env python3\n\"\"\"unidep - Unified Conda and Pip requirements management.\n\nThis module provides setuptools integration for unidep.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path, PurePath\nfrom typing import TYPE_CHECKING, NamedTuple\n\nfrom ruamel.yaml import YAML\n\nfrom unidep._dependencies_parsing import (\n    DependencyEntry,\n    _load,\n    get_local_dependencies,\n    parse_requirements,\n)\nfrom unidep._dependency_selection import (\n    collapse_selected_universals,\n    select_pip_requirements,\n)\nfrom unidep.utils import (\n    UnsupportedPlatformError,\n    build_pep508_environment_marker,\n    identify_current_platform,\n    is_pip_installable,\n    package_name_from_path,\n    parse_folder_or_filename,\n    split_path_and_extras,\n    warn,\n)\n\nif TYPE_CHECKING:\n    import sys\n\n    from setuptools import Distribution\n\n    from unidep.platform_definitions import Platform, Spec\n\n    if sys.version_info >= (3, 8):\n        from typing import Literal\n    else:\n        from typing_extensions import Literal\n\n\ndef filter_python_dependencies(\n    entries: list[DependencyEntry],\n    platforms: list[Platform] | None = None,\n) -> list[str]:\n    \"\"\"Filter out conda dependencies and return only pip dependencies.\n\n    Examples\n    --------\n    >>> requirements = parse_requirements(\"requirements.yaml\")\n    >>> python_deps = filter_python_dependencies(\n    ...     requirements.dependency_entries, requirements.platforms\n    ... )\n\n    \"\"\"\n    if isinstance(entries, dict):\n        msg = (\n            \"`filter_python_dependencies()` now requires dependency entries from \"\n            \"`parse_requirements(...).dependency_entries`, not the output of \"\n            \"`resolve_conflicts()`.\"\n        )\n        raise TypeError(msg)\n    entries = list(entries)\n    selected = collapse_selected_universals(\n        select_pip_requirements(entries, platforms),\n        platforms,\n    )\n    pip_deps: list[str] = []\n    by_spec: dict[Spec, list[Platform | None]] = {}\n    for _platform, candidates in selected.items():\n        for candidate in candidates:\n            by_spec.setdefault(candidate.spec, []).append(_platform)\n\n    for spec, _platforms in by_spec.items():\n        dep_str = spec.name_with_pin(is_pip=True)\n        if _platforms != [None] and all(\n            platform is not None for platform in _platforms\n        ):\n            selector = build_pep508_environment_marker(_platforms)  # type: ignore[arg-type]\n            dep_str = f\"{dep_str}; {selector}\"\n        pip_deps.append(dep_str)\n    return sorted(pip_deps)\n\n\nclass Dependencies(NamedTuple):\n    dependencies: list[str]\n    extras: dict[str, list[str]]\n\n\ndef _path_to_file_uri(path: PurePath) -> str:\n    \"\"\"Return a RFC 8089 compliant file URI for an absolute path.\"\"\"\n    # Keep in sync with CI helper and discussion in\n    # https://github.com/basnijholt/unidep/pull/214#issuecomment-2568663364\n    if isinstance(path, Path):\n        target = path if path.is_absolute() else path.resolve()\n        return target.as_uri()\n\n    uri_path = path.as_posix().lstrip(\"/\")\n    return f\"file:///{uri_path.replace(' ', '%20')}\"\n\n\ndef get_python_dependencies(  # noqa: PLR0912\n    filename: str\n    | Path\n    | Literal[\"requirements.yaml\", \"pyproject.toml\"] = \"requirements.yaml\",  # noqa: PYI051\n    *,\n    verbose: bool = False,\n    ignore_pins: list[str] | None = None,\n    overwrite_pins: list[str] | None = None,\n    skip_dependencies: list[str] | None = None,\n    platforms: list[Platform] | None = None,\n    raises_if_missing: bool = True,\n    include_local_dependencies: bool = False,\n) -> Dependencies:\n    \"\"\"Extract Python (pip) requirements from a `requirements.yaml` or `pyproject.toml` file.\"\"\"  # noqa: E501\n    try:\n        p = parse_folder_or_filename(filename)\n    except FileNotFoundError:\n        if raises_if_missing:\n            raise\n        return Dependencies(dependencies=[], extras={})\n\n    requirements = parse_requirements(\n        p.path,\n        ignore_pins=ignore_pins,\n        overwrite_pins=overwrite_pins,\n        skip_dependencies=skip_dependencies,\n        verbose=verbose,\n        extras=\"*\",\n    )\n    if not platforms:\n        platforms = list(requirements.platforms)\n    dependencies = filter_python_dependencies(\n        requirements.dependency_entries,\n        platforms,\n    )\n    # TODO[Bas]: This currently doesn't correctly handle  # noqa: TD004, TD003, FIX002\n    # conflicts between sections in the extras and the main dependencies.\n    extras = {\n        section: filter_python_dependencies(entries, platforms)\n        for section, entries in requirements.optional_dependency_entries.items()\n    }\n    # Always process local dependencies to handle PyPI alternatives\n    yaml = YAML(typ=\"rt\")\n    data = _load(p.path, yaml)\n\n    # Process each local dependency\n    for local_dep_obj in get_local_dependencies(data):\n        if local_dep_obj.use == \"skip\":\n            continue\n        if local_dep_obj.use == \"pypi\":\n            # Already added to pip dependencies when parsing requirements.\n            continue\n        local_path, extras_list = split_path_and_extras(local_dep_obj.local)\n        abs_local = (p.path.parent / local_path).resolve()\n\n        # If include_local_dependencies is False (UNIDEP_SKIP_LOCAL_DEPS=1),\n        # always use PyPI alternative if available, skip otherwise\n        if not include_local_dependencies:\n            if local_dep_obj.pypi:\n                dependencies.append(local_dep_obj.pypi)\n            continue\n\n        # Original behavior when include_local_dependencies is True\n        # Handle wheel and zip files\n        if abs_local.suffix in (\".whl\", \".zip\"):\n            if abs_local.exists():\n                # Local wheel exists - use it\n                uri = _path_to_file_uri(abs_local)\n                dependencies.append(f\"{abs_local.name} @ {uri}\")\n            elif local_dep_obj.pypi:\n                # Wheel doesn't exist - use PyPI alternative\n                dependencies.append(local_dep_obj.pypi)\n            continue\n\n        # Check if local path exists\n        if abs_local.exists() and is_pip_installable(abs_local):\n            # Local development - use file:// URL\n            name = package_name_from_path(abs_local)\n            uri = _path_to_file_uri(abs_local)\n            dep_str = f\"{name} @ {uri}\"\n            if extras_list:\n                dep_str = f\"{name}[{','.join(extras_list)}] @ {uri}\"\n            dependencies.append(dep_str)\n        elif local_dep_obj.pypi:\n            # Built wheel - local path doesn't exist, use PyPI alternative\n            dependencies.append(local_dep_obj.pypi)\n        # else: path doesn't exist and no PyPI alternative - skip\n\n    return Dependencies(dependencies=dependencies, extras=extras)\n\n\ndef _deps(requirements_file: Path) -> Dependencies:  # pragma: no cover\n    try:\n        platforms = [identify_current_platform()]\n    except UnsupportedPlatformError:\n        warn(\n            \"Could not identify the current platform.\"\n            \" This may result in selecting all platforms.\"\n            \" Please report this issue at\"\n            \" https://github.com/basnijholt/unidep/issues\",\n        )\n        # We don't know the current platform, so we can't filter out.\n        # This will result in selecting all platforms. But this is better\n        # than failing.\n        platforms = None\n\n    skip_local_dependencies = bool(os.getenv(\"UNIDEP_SKIP_LOCAL_DEPS\"))\n    verbose = bool(os.getenv(\"UNIDEP_VERBOSE\"))\n    return get_python_dependencies(\n        requirements_file,\n        platforms=platforms,\n        raises_if_missing=False,\n        verbose=verbose,\n        include_local_dependencies=not skip_local_dependencies,\n    )\n\n\ndef _setuptools_finalizer(dist: Distribution) -> None:  # pragma: no cover\n    \"\"\"Entry point called by setuptools to get the dependencies for a project.\"\"\"\n    # PEP 517 says that \"All hooks are run with working directory set to the\n    # root of the source tree\".\n    project_root = Path.cwd()\n    try:\n        requirements_file = parse_folder_or_filename(project_root).path\n    except FileNotFoundError:\n        return\n    if requirements_file.exists() and dist.install_requires:  # type: ignore[attr-defined]\n        msg = (\n            \"You have a `requirements.yaml` file in your project root or\"\n            \" configured unidep in `pyproject.toml` with `[tool.unidep]`,\"\n            \" but you are also using setuptools' `install_requires`.\"\n            \" Remove the `install_requires` line from `setup.py`.\"\n        )\n        raise RuntimeError(msg)\n\n    deps = _deps(requirements_file)\n    dist.install_requires = deps.dependencies  # type: ignore[attr-defined]\n\n    if deps.extras:\n        dist.extras_require = deps.extras  # type: ignore[attr-defined]\n"
  },
  {
    "path": "unidep/_version.py",
    "content": "\"\"\"unidep - Unified Conda and Pip requirements management.\"\"\"\n\n__version__ = \"3.2.0\"\n"
  },
  {
    "path": "unidep/platform_definitions.py",
    "content": "\"\"\"unidep - Unified Conda and Pip requirements management.\n\nTypes and definitions for platforms, selectors, and markers.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport sys\nfrom typing import NamedTuple, cast\n\nif sys.version_info >= (3, 8):\n    from typing import Literal, get_args\nelse:  # pragma: no cover\n    from typing_extensions import Literal, get_args\n\n\nCondaPlatform = Literal[\"unix\", \"linux\", \"osx\", \"win\"]\nPlatform = Literal[\n    \"linux-64\",\n    \"linux-aarch64\",\n    \"linux-ppc64le\",\n    \"osx-64\",\n    \"osx-arm64\",\n    \"win-64\",\n]\nSelector = Literal[\n    \"linux64\",\n    \"aarch64\",\n    \"ppc64le\",\n    \"osx64\",\n    \"arm64\",\n    \"win64\",\n    \"win\",\n    \"unix\",\n    \"linux\",\n    \"osx\",\n    \"macos\",\n]\n# The following are also supported in conda-build but not in UniDep:\n# \"linux-32\" (32-bit x86 on Linux)\n# \"linux-64\" (64-bit x86 on Linux)\n# \"linux-ppc64\" (64-bit PowerPC on Linux)\n# \"linux-ppc64le\" (64-bit Little Endian PowerPC on Linux)\n# \"linux-s390x\" (64-bit IBM z Systems on Linux)\n# \"linux-armv6l\" (32-bit ARMv6 on Linux)\n# \"linux-armv7l\" (32-bit ARMv7 on Linux)\n# \"win-32\" (32-bit x86 Windows)\n# \"win-arm64\" (64-bit ARM on Windows)\nCondaPip = Literal[\"conda\", \"pip\"]\n\nVALID_SELECTORS = get_args(Selector)\n\nPEP508_MARKERS = {\n    \"linux-64\": \"sys_platform == 'linux' and platform_machine == 'x86_64'\",\n    \"linux-aarch64\": \"sys_platform == 'linux' and platform_machine == 'aarch64'\",\n    \"linux-ppc64le\": \"sys_platform == 'linux' and platform_machine == 'ppc64le'\",\n    \"osx-64\": \"sys_platform == 'darwin' and platform_machine == 'x86_64'\",\n    \"osx-arm64\": \"sys_platform == 'darwin' and platform_machine == 'arm64'\",\n    \"win-64\": \"sys_platform == 'win32' and platform_machine == 'AMD64'\",\n    (\"linux-64\", \"linux-aarch64\", \"linux-ppc64le\"): \"sys_platform == 'linux'\",\n    (\"osx-64\", \"osx-arm64\"): \"sys_platform == 'darwin'\",\n    (\n        \"linux-64\",\n        \"linux-aarch64\",\n        \"linux-ppc64le\",\n        \"osx-64\",\n        \"osx-arm64\",\n    ): \"sys_platform == 'linux' or sys_platform == 'darwin'\",\n}\n\n\n# The first element of each tuple is the only unique selector\nPLATFORM_SELECTOR_MAP: dict[Platform, list[Selector]] = {\n    \"linux-64\": [\"linux64\", \"unix\", \"linux\"],\n    \"linux-aarch64\": [\"aarch64\", \"unix\", \"linux\"],\n    \"linux-ppc64le\": [\"ppc64le\", \"unix\", \"linux\"],\n    # \"osx64\" is a selector unique to conda-build referring to\n    # platforms on macOS and the Python architecture is x86-64\n    \"osx-64\": [\"osx64\", \"osx\", \"macos\", \"unix\"],\n    \"osx-arm64\": [\"arm64\", \"osx\", \"macos\", \"unix\"],\n    \"win-64\": [\"win64\", \"win\"],\n}\n\nPLATFORM_SELECTOR_MAP_REVERSE: dict[Selector, set[Platform]] = {}\nfor _platform, _selectors in PLATFORM_SELECTOR_MAP.items():\n    for _selector in _selectors:\n        PLATFORM_SELECTOR_MAP_REVERSE.setdefault(_selector, set()).add(_platform)\n\n\ndef validate_selector(selector: Selector) -> None:\n    \"\"\"Check if a selector is valid.\"\"\"\n    valid_selectors = VALID_SELECTORS\n    if selector not in VALID_SELECTORS:\n        msg = f\"Invalid platform selector: `{selector}`, use one of `{valid_selectors}`\"\n        raise ValueError(msg)\n\n\ndef platforms_from_selector(selector: str) -> list[Platform]:\n    \"\"\"Extract platforms from a selector.\n\n    For example, selector can be ``'linux64 win64'`` or ``'osx'``.\n    \"\"\"\n    # we support a very limited set of selectors that adhere to platform only\n    # refs:\n    # https://docs.conda.io/projects/conda-build/en/latest/resources/define-metadata.html#preprocessing-selectors\n    # https://github.com/conda/conda-lock/blob/3d2bf356e2cf3f7284407423f7032189677ba9be/conda_lock/src_parser/selectors.py\n    platforms: set[Platform] = set()\n    for s in selector.split():\n        s = cast(\"Selector\", s)\n        platforms |= set(PLATFORM_SELECTOR_MAP_REVERSE[s])\n    return sorted(platforms)\n\n\nclass Spec(NamedTuple):\n    \"\"\"A dependency specification.\"\"\"\n\n    name: str\n    which: CondaPip\n    pin: str | None = None\n    identifier: str | None = None\n    # can be of type `Selector` but also space separated string of `Selector`s\n    selector: str | None = None\n\n    def platforms(self) -> list[Platform] | None:\n        \"\"\"Return the platforms for this dependency.\"\"\"\n        if self.selector is None:\n            return None\n        return platforms_from_selector(self.selector)\n\n    def pprint(self) -> str:\n        \"\"\"Pretty print the dependency.\"\"\"\n        result = f\"{self.name}\"\n        if self.pin is not None:\n            result += f\" {self.pin}\"\n        if self.selector is not None:\n            result += f\" # [{self.selector}]\"\n        return result\n\n    def name_with_pin(self, *, is_pip: bool = False) -> str:\n        \"\"\"Return the name with the pin.\"\"\"\n        result = f\"{self.name}\"\n        if self.pin is not None:\n            pin = self.pin\n            if is_pip:\n                pin = \",\".join(\n                    (\n                        f\"=={token[1:]}\"\n                        if token.startswith(\"=\") and not token.startswith(\"==\")\n                        else token\n                    )\n                    for token in pin.split(\",\")\n                )\n            result += f\" {pin}\"\n        return result\n"
  },
  {
    "path": "unidep/py.typed",
    "content": ""
  },
  {
    "path": "unidep/utils.py",
    "content": "\"\"\"unidep - Unified Conda and Pip requirements management.\n\nThis module provides utility functions used throughout the package.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport ast\nimport codecs\nimport configparser\nimport contextlib\nimport platform\nimport re\nimport sys\nimport warnings\nfrom collections import defaultdict\nfrom pathlib import Path\nfrom typing import Any, Literal, NamedTuple, cast\n\nfrom unidep._version import __version__\nfrom unidep.platform_definitions import (\n    PEP508_MARKERS,\n    Platform,\n    Selector,\n    Spec,\n    platforms_from_selector,\n    validate_selector,\n)\n\nif sys.version_info >= (3, 11):\n    import tomllib\nelse:  # pragma: no cover\n    import tomli as tomllib\n\n\ndef add_comment_to_file(\n    filename: str | Path,\n    extra_lines: list[str] | None = None,\n) -> None:\n    \"\"\"Add a comment to the top of a file.\"\"\"\n    if extra_lines is None:\n        extra_lines = []\n    with open(filename, \"r+\") as f:  # noqa: PTH123\n        content = f.read()\n        f.seek(0, 0)\n        command_line_args = \" \".join(sys.argv[1:])\n        txt = [\n            f\"# This file is created and managed by `unidep` {__version__}.\",\n            \"# For details see https://github.com/basnijholt/unidep\",\n            f\"# File generated with: `unidep {command_line_args}`\",\n            *extra_lines,\n        ]\n        content = \"\\n\".join(txt) + \"\\n\\n\" + content\n        f.write(content)\n\n\ndef remove_top_comments(filename: str | Path) -> None:\n    \"\"\"Removes the top comments (lines starting with '#') from a file.\"\"\"\n    with open(filename) as file:  # noqa: PTH123\n        lines = file.readlines()\n\n    first_non_comment = next(\n        (i for i, line in enumerate(lines) if not line.strip().startswith(\"#\")),\n        len(lines),\n    )\n    content_without_comments = lines[first_non_comment:]\n    with open(filename, \"w\") as file:  # noqa: PTH123\n        file.writelines(content_without_comments)\n\n\ndef escape_unicode(string: str) -> str:\n    \"\"\"Escape unicode characters.\"\"\"\n    return codecs.decode(string, \"unicode_escape\")\n\n\ndef is_pip_installable(folder: str | Path) -> bool:  # pragma: no cover\n    \"\"\"Determine if the project is pip installable.\n\n    Checks for existence of setup.py or [build-system] in pyproject.toml.\n    If the `toml` library is available, it is used to parse the `pyproject.toml` file.\n    If the `toml` library is not available, the function checks for the existence of\n    a line starting with \"[build-system]\". This does not handle the case where\n    [build-system] is inside of a multi-line literal string.\n    \"\"\"\n    path = Path(folder)\n    if (path / \"setup.py\").exists():\n        return True\n\n    pyproject_path = path / \"pyproject.toml\"\n    if pyproject_path.exists():\n        with pyproject_path.open(\"rb\") as file:\n            pyproject_data = tomllib.load(file)\n            return \"build-system\" in pyproject_data\n    return False\n\n\nclass UnsupportedPlatformError(Exception):\n    \"\"\"Raised when the current platform is not supported.\"\"\"\n\n\ndef identify_current_platform() -> Platform:\n    \"\"\"Detect the current platform.\"\"\"\n    system = platform.system().lower()\n    architecture = platform.machine().lower()\n\n    if system == \"linux\":\n        if architecture == \"x86_64\":\n            return \"linux-64\"\n        if architecture == \"aarch64\":\n            return \"linux-aarch64\"\n        if architecture == \"ppc64le\":\n            return \"linux-ppc64le\"\n        msg = f\"Unsupported Linux architecture `{architecture}`\"\n        raise UnsupportedPlatformError(msg)\n    if system == \"darwin\":\n        if architecture == \"x86_64\":\n            return \"osx-64\"\n        if architecture == \"arm64\":\n            return \"osx-arm64\"\n        msg = f\"Unsupported macOS architecture `{architecture}`\"\n        raise UnsupportedPlatformError(msg)\n    if system == \"windows\":\n        if \"64\" in architecture:\n            return \"win-64\"\n        msg = f\"Unsupported Windows architecture `{architecture}`\"\n        raise UnsupportedPlatformError(msg)\n    msg = f\"Unsupported operating system `{system}` with architecture `{architecture}`\"\n    raise UnsupportedPlatformError(msg)\n\n\ndef collect_selector_platforms(\n    requirements: dict[str, list[Spec]],\n    optional_dependencies: dict[str, dict[str, list[Spec]]] | None = None,\n) -> list[Platform]:\n    \"\"\"Collect all platforms referenced by dependency selectors.\"\"\"\n    selector_platforms: set[Platform] = set()\n\n    def _collect(specs_by_name: dict[str, list[Spec]]) -> None:\n        for specs in specs_by_name.values():\n            for spec in specs:\n                if spec.selector is None:\n                    continue\n                selector_platforms.update(platforms_from_selector(spec.selector))\n\n    _collect(requirements)\n    if optional_dependencies is not None:\n        for optional_specs in optional_dependencies.values():\n            _collect(optional_specs)\n    return sorted(selector_platforms)\n\n\ndef resolve_platforms(\n    *,\n    requested_platforms: list[Platform] | None,\n    declared_platforms: list[Platform] | set[Platform] | None = None,\n    selector_platforms: list[Platform] | set[Platform] | None = None,\n    default_current: bool = True,\n) -> list[Platform]:\n    \"\"\"Resolve effective platforms with a shared precedence policy.\n\n    Precedence is:\n    1) explicitly requested platforms\n    2) declared platforms from requirements files\n    3) selector-derived platforms from dependency specs\n    4) current platform fallback (optional)\n    \"\"\"\n    for candidate in (requested_platforms, declared_platforms, selector_platforms):\n        if candidate:\n            return sorted(set(candidate))\n    if default_current:\n        return [identify_current_platform()]\n    return []\n\n\ndef build_pep508_environment_marker(\n    platforms: list[Platform | tuple[Platform, ...]],\n) -> str:\n    \"\"\"Generate a PEP 508 selector for a list of platforms.\"\"\"\n    sorted_platforms = tuple(sorted(platforms))\n    if sorted_platforms in PEP508_MARKERS:\n        return PEP508_MARKERS[sorted_platforms]  # type: ignore[index]\n    environment_markers = [\n        PEP508_MARKERS[platform]\n        for platform in sorted(sorted_platforms)\n        if platform in PEP508_MARKERS\n    ]\n    return \" or \".join(environment_markers)\n\n\nclass ParsedPackageStr(NamedTuple):\n    \"\"\"A package name and version pinning.\"\"\"\n\n    name: str\n    pin: str | None = None\n    # can be of type `Selector` but also space separated string of `Selector`s\n    selector: str | None = None\n\n\ndef parse_package_str(package_str: str) -> ParsedPackageStr:\n    \"\"\"Splits a string into package name, version pinning, and platform selector.\"\"\"\n    # Regex to match package name, version pinning, and optionally platform selector\n    # Note: the name_pattern currently allows for paths and extras, however,\n    # paths cannot contain spaces or contain brackets.\n    name_pattern = r\"[a-zA-Z0-9_.\\-/]+(\\[[a-zA-Z0-9_.,\\-]+\\])?\"\n    version_pin_pattern = r\".*?\"\n    selector_pattern = r\"[a-z0-9\\s]+\"\n    pattern = rf\"({name_pattern})\\s*({version_pin_pattern})?(:({selector_pattern}))?$\"\n    match = re.match(pattern, package_str)\n\n    if match:\n        package_name = match.group(1).strip()\n        version_pin = match.group(3).strip() if match.group(3) else None\n        selector = match.group(5).strip() if match.group(5) else None\n\n        if selector is not None:\n            for s in selector.split():\n                validate_selector(cast(\"Selector\", s))\n\n        return ParsedPackageStr(\n            package_name,\n            version_pin,\n            selector,\n        )\n\n    msg = f\"Invalid package string: '{package_str}'\"\n    raise ValueError(msg)\n\n\ndef package_name_from_setup_cfg(file_path: Path) -> str:\n    \"\"\"Read the package name from ``setup.cfg`` metadata.\"\"\"\n    config = configparser.ConfigParser()\n    config.read(file_path)\n    name = config.get(\"metadata\", \"name\", fallback=None)\n    if name is None:\n        msg = \"Could not find the package name in the setup.cfg file.\"\n        raise KeyError(msg)\n    return name\n\n\ndef package_name_from_setup_py(file_path: Path) -> str:\n    \"\"\"Read the package name from a simple ``setup.py`` AST.\"\"\"\n    with file_path.open() as f:\n        file_content = f.read()\n\n    tree = ast.parse(file_content)\n\n    def _string_literal(node: ast.expr) -> str | None:\n        if isinstance(node, ast.Constant) and isinstance(node.value, str):\n            return node.value\n        return None\n\n    class SetupVisitor(ast.NodeVisitor):\n        def __init__(self) -> None:\n            self.package_name: str | None = None\n\n        def visit_Call(self, node: ast.Call) -> None:  # noqa: N802\n            if isinstance(node.func, ast.Name) and node.func.id == \"setup\":\n                for keyword in node.keywords:\n                    if keyword.arg == \"name\":\n                        self.package_name = _string_literal(keyword.value)\n                        if self.package_name is not None:\n                            return\n\n    visitor = SetupVisitor()\n    visitor.visit(tree)\n    if visitor.package_name is None:\n        msg = \"Could not find the package name in the setup.py file.\"\n        raise KeyError(msg)\n    return visitor.package_name\n\n\ndef package_name_from_pyproject_toml(file_path: Path) -> str:\n    \"\"\"Read project name from ``pyproject.toml`` (PEP 621 or Poetry).\"\"\"\n    with file_path.open(\"rb\") as f:\n        data = tomllib.load(f)\n    with contextlib.suppress(KeyError):\n        return data[\"project\"][\"name\"]\n    with contextlib.suppress(KeyError):\n        return data[\"tool\"][\"poetry\"][\"name\"]\n    msg = f\"Could not find the package name in the pyproject.toml file: {data}.\"\n    raise KeyError(msg)\n\n\ndef package_name_from_path(path: Path) -> str:\n    \"\"\"Get the package name from ``pyproject.toml``, ``setup.cfg``, or ``setup.py``.\"\"\"\n    pyproject_toml = path / \"pyproject.toml\"\n    if pyproject_toml.exists():\n        with contextlib.suppress(\n            KeyError,\n            OSError,\n            TypeError,\n            UnicodeError,\n            tomllib.TOMLDecodeError,\n        ):\n            return package_name_from_pyproject_toml(pyproject_toml)\n\n    setup_cfg = path / \"setup.cfg\"\n    if setup_cfg.exists():\n        with contextlib.suppress(\n            KeyError,\n            OSError,\n            UnicodeError,\n            configparser.Error,\n        ):\n            return package_name_from_setup_cfg(setup_cfg)\n\n    setup_py = path / \"setup.py\"\n    if setup_py.exists():\n        with contextlib.suppress(\n            KeyError,\n            OSError,\n            SyntaxError,\n            UnicodeError,\n            ValueError,\n        ):\n            return package_name_from_setup_py(setup_py)\n\n    return path.name\n\n\ndef _simple_warning_format(\n    message: Warning | str,\n    category: type[Warning],  # noqa: ARG001\n    filename: str,\n    lineno: int,\n    line: str | None = None,  # noqa: ARG001\n) -> str:  # pragma: no cover\n    \"\"\"Format warnings without code context.\"\"\"\n    return (\n        f\"---------------------\\n\"\n        f\"⚠️  *** WARNING *** ⚠️\\n\"\n        f\"{message}\\n\"\n        f\"Location: {filename}:{lineno}\\n\"\n        f\"---------------------\\n\"\n    )\n\n\ndef warn(\n    message: str | Warning,\n    category: type[Warning] = UserWarning,\n    stacklevel: int = 1,\n) -> None:\n    \"\"\"Emit a warning with a custom format specific to this package.\"\"\"\n    original_format = warnings.formatwarning\n    warnings.formatwarning = _simple_warning_format\n    try:\n        warnings.warn(message, category, stacklevel=stacklevel + 1)\n    finally:\n        warnings.formatwarning = original_format\n\n\ndef selector_from_comment(comment: str) -> str | None:\n    \"\"\"Extract a valid selector from a comment.\"\"\"\n    multiple_brackets_pat = re.compile(r\"#.*\\].*\\[\")  # Detects multiple brackets\n    if multiple_brackets_pat.search(comment):\n        msg = f\"Multiple bracketed selectors found in comment: '{comment}'\"\n        raise ValueError(msg)\n\n    sel_pat = re.compile(r\"#\\s*\\[([^\\[\\]]+)\\]\")\n    m = sel_pat.search(comment)\n    if not m:\n        return None\n    selectors = m.group(1).strip().split()\n    for s in selectors:\n        validate_selector(cast(\"Selector\", s))\n    return \" \".join(selectors)\n\n\ndef extract_matching_platforms(comment: str) -> list[Platform]:\n    \"\"\"Get all platforms matching a comment.\"\"\"\n    selector = selector_from_comment(comment)\n    if selector is None:\n        return []\n    return platforms_from_selector(selector)\n\n\ndef unidep_configured_in_toml(path: Path) -> bool:\n    \"\"\"Check if dependencies are specified in pyproject.toml.\"\"\"\n    with path.open(\"rb\") as f:\n        data = tomllib.load(f)\n    return bool(data.get(\"tool\", {}).get(\"unidep\", {}))\n\n\ndef split_path_and_extras(input_str: str | Path) -> tuple[Path, list[str]]:\n    \"\"\"Parse a string of the form `path/to/file[extra1,extra2]` into parts.\n\n    Returns a tuple of the `pathlib.Path` and a list of extras\n    \"\"\"\n    if isinstance(input_str, Path):\n        input_str = str(input_str)\n\n    if not input_str:  # Check for empty string\n        return Path(), []\n\n    pattern = r\"^(.+?)(?:\\[([^\\[\\]]+)\\])?$\"\n    match = re.search(pattern, input_str)\n\n    if match is None:  # pragma: no cover\n        # I don't think this is possible, but just in case\n        return Path(), []\n\n    path = Path(match.group(1))\n    extras = match.group(2)\n    if not extras:\n        return path, []\n    extras = [extra.strip() for extra in extras.split(\",\")]\n    return path, extras\n\n\nclass PathWithExtras(NamedTuple):\n    \"\"\"A dependency file and extras.\"\"\"\n\n    path: Path\n    extras: list[str]\n\n    @property\n    def path_with_extras(self) -> Path:\n        \"\"\"Path including extras, e.g., `path/to/file[test,docs]`.\"\"\"\n        if not self.extras:\n            return self.path\n        return Path(f\"{self.path}[{','.join(self.extras)}]\")\n\n    def resolved(self) -> PathWithExtras:\n        \"\"\"Resolve the path and extras.\"\"\"\n        return PathWithExtras(self.path.resolve(), self.extras)\n\n    def canonicalized(self) -> PathWithExtras:\n        \"\"\"Resolve path and normalize extras for deterministic graph keys.\"\"\"\n        return PathWithExtras(self.path.resolve(), sorted(set(self.extras)))\n\n    def __hash__(self) -> int:\n        \"\"\"Hash the path and extras.\"\"\"\n        return hash((self.path, tuple(sorted(self.extras))))\n\n    def __eq__(self, other: object) -> bool:\n        \"\"\"Check if two `PathWithExtras` are equal.\"\"\"\n        if not isinstance(other, PathWithExtras):\n            return NotImplemented\n        return self.path == other.path and set(self.extras) == set(other.extras)\n\n\nLocalDependencyUse = Literal[\"local\", \"pypi\", \"skip\"]\n\n\nclass LocalDependency(NamedTuple):\n    \"\"\"A local dependency with optional PyPI alternative and `use` mode.\"\"\"\n\n    local: str\n    pypi: str | None = None\n    use: LocalDependencyUse = \"local\"\n\n\ndef parse_folder_or_filename(folder_or_file: str | Path) -> PathWithExtras:\n    \"\"\"Get the path to `requirements.yaml` or `pyproject.toml` file.\"\"\"\n    folder_or_file, extras = split_path_and_extras(folder_or_file)\n    path = Path(folder_or_file)\n    if path.is_dir():\n        fname_yaml = path / \"requirements.yaml\"\n        if fname_yaml.exists():\n            return PathWithExtras(fname_yaml, extras)\n        fname_toml = path / \"pyproject.toml\"\n        if fname_toml.exists() and unidep_configured_in_toml(fname_toml):\n            return PathWithExtras(fname_toml, extras)\n        msg = (\n            f\"File `{fname_yaml}` or `{fname_toml}` (with unidep configuration)\"\n            f\" not found in `{folder_or_file}`.\"\n        )\n        raise FileNotFoundError(msg)\n    if not path.exists():\n        msg = f\"File `{path}` not found.\"\n        raise FileNotFoundError(msg)\n    return PathWithExtras(path, extras)\n\n\ndef defaultdict_to_dict(d: defaultdict | Any) -> dict:\n    \"\"\"Convert (nested) defaultdict to (nested) dict.\"\"\"\n    if isinstance(d, defaultdict):\n        d = {key: defaultdict_to_dict(value) for key, value in d.items()}\n    return d\n\n\ndef get_package_version(package_name: str) -> str | None:\n    \"\"\"Returns the version of the given package.\n\n    Parameters\n    ----------\n    package_name\n        The name of the package to find the version of.\n\n    Returns\n    -------\n    The version of the package, or None if the package is not found.\n\n    \"\"\"\n    if sys.version_info >= (3, 8):\n        import importlib.metadata\n\n        try:\n            return importlib.metadata.version(package_name)\n        except importlib.metadata.PackageNotFoundError:\n            return None\n    else:  # pragma: no cover\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", DeprecationWarning)\n            import pkg_resources\n\n        try:\n            return pkg_resources.get_distribution(package_name).version\n        except pkg_resources.DistributionNotFound:\n            return None\n"
  }
]