├── tests ├── fixtures │ ├── pipenv │ │ ├── minimal │ │ │ └── Pipfile │ │ ├── existing_project │ │ │ ├── pyproject.toml │ │ │ └── Pipfile │ │ ├── full │ │ │ ├── pyproject.toml │ │ │ └── Pipfile │ │ └── with_lock_file │ │ │ ├── Pipfile │ │ │ └── Pipfile.lock │ ├── poetry │ │ ├── build_backend_uv │ │ │ ├── text_file_sdist.txt │ │ │ ├── include_nowhere │ │ │ │ └── foo.py │ │ │ ├── include_sdist │ │ │ │ └── foo.py │ │ │ ├── include_sdist_2 │ │ │ │ └── foo.py │ │ │ ├── include_sdist_3 │ │ │ │ └── foo.py │ │ │ ├── include_sdist_4 │ │ │ │ └── foo.py │ │ │ ├── packages_nowhere │ │ │ │ ├── foo.py │ │ │ │ └── __init__.py │ │ │ ├── packages_sdist │ │ │ │ ├── foo.py │ │ │ │ └── __init__.py │ │ │ ├── packages_sdist_2 │ │ │ │ ├── foo.py │ │ │ │ └── __init__.py │ │ │ ├── FILE_WITHOUT_EXTENSION_SDIST │ │ │ ├── packages_sdist_wheel │ │ │ │ ├── foo.py │ │ │ │ └── __init__.py │ │ │ ├── packages_sdist_wheel_2 │ │ │ │ ├── foo.py │ │ │ │ └── __init__.py │ │ │ ├── packages_glob_nowhere │ │ │ │ ├── __init__.py │ │ │ │ └── foo │ │ │ │ │ ├── bar.py │ │ │ │ │ └── __init__.py │ │ │ ├── packages_glob_sdist │ │ │ │ ├── __init__.py │ │ │ │ └── foo │ │ │ │ │ ├── bar.py │ │ │ │ │ └── __init__.py │ │ │ ├── packages_glob_sdist_2 │ │ │ │ ├── __init__.py │ │ │ │ └── foo │ │ │ │ │ ├── bar.py │ │ │ │ │ └── __init__.py │ │ │ ├── INCLUDE_FILE_WITHOUT_EXTENSION_SDIST │ │ │ ├── packages_sdist_wheel_with_excluded_files │ │ │ │ ├── bar.py │ │ │ │ ├── foo.py │ │ │ │ ├── __init__.py │ │ │ │ └── foobar │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── barfoo.py │ │ │ └── pyproject.toml │ │ ├── build_backend_hatch │ │ │ ├── include_sdist │ │ │ │ └── foo.py │ │ │ ├── include_wheel │ │ │ │ └── foo.py │ │ │ ├── packages_sdist │ │ │ │ └── foo.py │ │ │ ├── packages_wheel │ │ │ │ └── foo.py │ │ │ ├── text_file_sdist.txt │ │ │ ├── text_file_wheel.txt │ │ │ ├── include_nowhere │ │ │ │ └── foo.py │ │ │ ├── include_sdist_2 │ │ │ │ └── foo.py │ │ │ ├── include_sdist_3 │ │ │ │ └── foo.py │ │ │ ├── include_sdist_4 │ │ │ │ └── foo.py │ │ │ ├── include_sdist_wheel │ │ │ │ └── foo.py │ │ │ ├── include_wheel_2 │ │ │ │ └── foo.py │ │ │ ├── packages_nowhere │ │ │ │ └── foo.py │ │ │ ├── packages_sdist_2 │ │ │ │ └── foo.py │ │ │ ├── packages_wheel_2 │ │ │ │ └── foo.py │ │ │ ├── text_file_sdist_wheel.txt │ │ │ ├── packages_glob_sdist │ │ │ │ └── foo │ │ │ │ │ └── bar.py │ │ │ ├── packages_glob_wheel │ │ │ │ └── foo │ │ │ │ │ └── bar.py │ │ │ ├── packages_sdist_wheel │ │ │ │ └── foo.py │ │ │ ├── packages_sdist_wheel_2 │ │ │ │ └── foo.py │ │ │ ├── packages_to_sdist_wheel │ │ │ │ └── foo.py │ │ │ ├── packages_glob_nowhere │ │ │ │ └── foo │ │ │ │ │ └── bar.py │ │ │ ├── packages_glob_sdist_2 │ │ │ │ └── foo │ │ │ │ │ └── bar.py │ │ │ ├── packages_glob_sdist_wheel │ │ │ │ └── foo │ │ │ │ │ └── bar.py │ │ │ ├── packages_glob_wheel_2 │ │ │ │ └── foo │ │ │ │ │ └── bar.py │ │ │ ├── from │ │ │ │ ├── packages_from_sdist_wheel │ │ │ │ │ └── foo.py │ │ │ │ ├── packages_from_to_sdist_wheel │ │ │ │ │ └── foo.py │ │ │ │ └── packages_glob_from_to_sdist_wheel │ │ │ │ │ └── foo │ │ │ │ │ └── bar.py │ │ │ ├── packages_glob_sdist_wheel_2 │ │ │ │ └── foo │ │ │ │ │ └── bar.py │ │ │ ├── packages_glob_to_sdist_wheel │ │ │ │ └── foo │ │ │ │ │ └── bar.py │ │ │ ├── packages_sdist_wheel_with_excluded_files │ │ │ │ ├── bar.py │ │ │ │ ├── foo.py │ │ │ │ └── foobar │ │ │ │ │ └── barfoo.py │ │ │ └── pyproject.toml │ │ ├── build_backend_uv_incompatible │ │ │ ├── include_wheel │ │ │ │ ├── foo.py │ │ │ │ └── __init__.py │ │ │ ├── packages_wheel │ │ │ │ ├── foo.py │ │ │ │ └── __init__.py │ │ │ ├── text_file_sdist.txt │ │ │ ├── text_file_wheel.txt │ │ │ ├── include_sdist_wheel │ │ │ │ ├── foo.py │ │ │ │ └── __init__.py │ │ │ ├── include_wheel_2 │ │ │ │ ├── foo.py │ │ │ │ └── __init__.py │ │ │ ├── packages_wheel_2 │ │ │ │ ├── foo.py │ │ │ │ └── __init__.py │ │ │ ├── text_file_sdist_wheel.txt │ │ │ ├── packages_glob_wheel │ │ │ │ ├── __init__.py │ │ │ │ └── foo │ │ │ │ │ └── bar.py │ │ │ ├── packages_to_sdist_wheel │ │ │ │ ├── foo.py │ │ │ │ └── __init__.py │ │ │ ├── packages_without_init │ │ │ │ └── foo.py │ │ │ ├── packages_glob_sdist_wheel │ │ │ │ ├── foo │ │ │ │ │ └── bar.py │ │ │ │ └── __init__.py │ │ │ ├── packages_glob_wheel_2 │ │ │ │ ├── __init__.py │ │ │ │ └── foo │ │ │ │ │ └── bar.py │ │ │ ├── packages_without_init_root │ │ │ │ ├── foo.py │ │ │ │ └── bar │ │ │ │ │ ├── foobar.py │ │ │ │ │ └── __init__.py │ │ │ ├── from │ │ │ │ ├── packages_from_sdist_wheel │ │ │ │ │ ├── foo.py │ │ │ │ │ └── __init__.py │ │ │ │ ├── packages_from_to_sdist_wheel │ │ │ │ │ ├── foo.py │ │ │ │ │ └── __init__.py │ │ │ │ ├── packages_from_without_init │ │ │ │ │ └── foo.py │ │ │ │ ├── packages_from_without_init_root │ │ │ │ │ ├── foo.py │ │ │ │ │ └── bar │ │ │ │ │ │ ├── foobar.py │ │ │ │ │ │ └── __init__.py │ │ │ │ └── packages_glob_from_to_sdist_wheel │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── foo │ │ │ │ │ └── bar.py │ │ │ ├── packages_glob_sdist_wheel_2 │ │ │ │ ├── __init__.py │ │ │ │ └── foo │ │ │ │ │ └── bar.py │ │ │ ├── packages_glob_to_sdist_wheel │ │ │ │ ├── __init__.py │ │ │ │ └── foo │ │ │ │ │ └── bar.py │ │ │ └── pyproject.toml │ │ ├── with_lock_file │ │ │ ├── poetry.toml │ │ │ └── pyproject.toml │ │ ├── with_migration_errors │ │ │ ├── poetry.toml │ │ │ └── pyproject.toml │ │ ├── with_migration_warnings │ │ │ ├── poetry.toml │ │ │ └── pyproject.toml │ │ ├── pep_621_no_poetry_section_with_lock_file │ │ │ ├── poetry.toml │ │ │ └── pyproject.toml │ │ ├── minimal │ │ │ └── pyproject.toml │ │ ├── existing_project │ │ │ └── pyproject.toml │ │ └── pep_621 │ │ │ └── pyproject.toml │ ├── pip │ │ ├── full │ │ │ ├── constraints-2.txt │ │ │ ├── constraints.txt │ │ │ ├── requirements-typing.txt │ │ │ ├── requirements-dev.txt │ │ │ └── requirements.txt │ │ └── existing_project │ │ │ ├── pyproject.toml │ │ │ └── requirements.txt │ ├── pip_tools │ │ ├── with_lock_file │ │ │ ├── requirements.in │ │ │ ├── requirements-dev.in │ │ │ ├── requirements-typing.in │ │ │ ├── requirements.txt │ │ │ ├── requirements-typing.txt │ │ │ └── requirements-dev.txt │ │ ├── existing_project │ │ │ ├── requirements.in │ │ │ ├── pyproject.toml │ │ │ └── requirements.txt │ │ └── full │ │ │ ├── requirements-dev.in │ │ │ ├── requirements-typing.in │ │ │ ├── requirements.in │ │ │ ├── requirements-dev.txt │ │ │ ├── requirements-typing.txt │ │ │ └── requirements.txt │ └── uv │ │ ├── with_lock │ │ ├── uv.lock │ │ └── pyproject.toml │ │ └── minimal │ │ └── pyproject.toml └── common │ └── mod.rs ├── .github ├── FUNDING.yml ├── zizmor.yml ├── renovate.json5 └── workflows │ ├── validate-ci-workflows.yml │ ├── validate-renovate-config.yml │ ├── ci.yml │ └── release.yml ├── rust-toolchain.toml ├── .gitignore ├── src ├── bin │ └── migrate-to-uv.rs ├── schema │ ├── mod.rs │ ├── utils.rs │ ├── hatch.rs │ ├── pyproject.rs │ ├── pep_621.rs │ ├── uv.rs │ ├── pipenv.rs │ └── poetry.rs ├── lib.rs ├── converters │ ├── pipenv │ │ ├── project.rs │ │ ├── sources.rs │ │ ├── mod.rs │ │ └── dependencies.rs │ ├── pip │ │ ├── dependencies.rs │ │ └── mod.rs │ ├── poetry │ │ ├── sources.rs │ │ ├── project.rs │ │ ├── version.rs │ │ └── dependencies.rs │ └── pyproject_updater.rs ├── logger.rs ├── errors.rs ├── cli.rs └── toml.rs ├── scripts ├── copy-changelog.sh ├── copy-contributing.sh └── bump-version.sh ├── .editorconfig ├── Makefile ├── LICENSE ├── Cargo.toml ├── .pre-commit-config.yaml ├── pyproject.toml ├── mkdocs.yml ├── README.md ├── CONTRIBUTING.md ├── docs ├── CONTRIBUTING.md ├── index.md ├── usage.md ├── configuration.md ├── CHANGELOG.md └── supported-package-managers.md └── CHANGELOG.md /tests/fixtures/pipenv/minimal/Pipfile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: mkniewallner 2 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.92" 3 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/text_file_sdist.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/pip/full/constraints-2.txt: -------------------------------------------------------------------------------- 1 | zstandard==0.23.0 2 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/include_sdist/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/include_wheel/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_sdist/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_wheel/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/text_file_sdist.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/text_file_wheel.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/include_nowhere/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/include_sdist/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/include_sdist_2/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/include_sdist_3/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/include_sdist_4/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_nowhere/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_sdist/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_sdist_2/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # mkdocs documentation 4 | /site 5 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/include_nowhere/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/include_sdist_2/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/include_sdist_3/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/include_sdist_4/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/include_sdist_wheel/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/include_wheel_2/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_nowhere/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_sdist_2/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_wheel_2/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/text_file_sdist_wheel.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/FILE_WITHOUT_EXTENSION_SDIST: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_nowhere/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_sdist/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_sdist_2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_sdist_wheel/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_sdist_wheel_2/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/pip_tools/with_lock_file/requirements.in: -------------------------------------------------------------------------------- 1 | arrow>=1.2.3 2 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_glob_sdist/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_glob_wheel/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_sdist_wheel/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_sdist_wheel_2/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_to_sdist_wheel/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_glob_nowhere/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_glob_nowhere/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_glob_sdist/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_glob_sdist/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_glob_sdist_2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_glob_sdist_2/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_sdist_wheel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_sdist_wheel_2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/include_wheel/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_wheel/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/text_file_sdist.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/text_file_wheel.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/pip/full/constraints.txt: -------------------------------------------------------------------------------- 1 | h11==0.14.0 2 | httpcore==1.0.7 3 | -------------------------------------------------------------------------------- /tests/fixtures/pip_tools/existing_project/requirements.in: -------------------------------------------------------------------------------- 1 | arrow>=1.2.3 2 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_glob_nowhere/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_glob_sdist_2/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_glob_sdist_wheel/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_glob_wheel_2/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/INCLUDE_FILE_WITHOUT_EXTENSION_SDIST: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_glob_nowhere/foo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_glob_sdist/foo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_glob_sdist_2/foo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/include_sdist_wheel/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/include_wheel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/include_wheel_2/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_wheel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_wheel_2/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/text_file_sdist_wheel.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/pip_tools/full/requirements-dev.in: -------------------------------------------------------------------------------- 1 | pytest>=8.3.4 2 | ruff==0.8.4 3 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/from/packages_from_sdist_wheel/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/from/packages_from_to_sdist_wheel/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_glob_sdist_wheel_2/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_glob_to_sdist_wheel/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/include_sdist_wheel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/include_wheel_2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_glob_wheel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_glob_wheel/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_to_sdist_wheel/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_wheel_2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_without_init/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_sdist_wheel_with_excluded_files/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_sdist_wheel_with_excluded_files/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_glob_sdist_wheel/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_glob_wheel_2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_glob_wheel_2/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_to_sdist_wheel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_without_init_root/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/with_lock_file/poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/from/packages_glob_from_to_sdist_wheel/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_sdist_wheel_with_excluded_files/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_sdist_wheel_with_excluded_files/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_sdist_wheel_with_excluded_files/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/from/packages_from_sdist_wheel/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/from/packages_from_to_sdist_wheel/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/from/packages_from_without_init/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_glob_sdist_wheel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_glob_sdist_wheel_2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_glob_sdist_wheel_2/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_glob_to_sdist_wheel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_glob_to_sdist_wheel/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_without_init_root/bar/foobar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_sdist_wheel_with_excluded_files/foobar/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/packages_sdist_wheel_with_excluded_files/foobar/barfoo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/from/packages_from_sdist_wheel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/from/packages_from_to_sdist_wheel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/from/packages_from_without_init_root/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/packages_without_init_root/bar/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/with_migration_errors/poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/with_migration_warnings/poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/packages_sdist_wheel_with_excluded_files/foobar/barfoo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/from/packages_from_without_init_root/bar/foobar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/from/packages_glob_from_to_sdist_wheel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/from/packages_glob_from_to_sdist_wheel/foo/bar.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/pip_tools/full/requirements-typing.in: -------------------------------------------------------------------------------- 1 | mypy==1.14.1 2 | types-jsonschema==4.23.0.20241208 3 | -------------------------------------------------------------------------------- /tests/fixtures/pip_tools/with_lock_file/requirements-dev.in: -------------------------------------------------------------------------------- 1 | -c requirements.txt 2 | factory-boy>=3.2.1 3 | -------------------------------------------------------------------------------- /tests/fixtures/pip_tools/with_lock_file/requirements-typing.in: -------------------------------------------------------------------------------- 1 | -c requirements-dev.txt 2 | mypy>=1.13.0 3 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/from/packages_from_without_init_root/bar/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/zizmor.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | unpinned-uses: 3 | config: 4 | policies: 5 | '*': hash-pin 6 | -------------------------------------------------------------------------------- /tests/fixtures/pip/full/requirements-typing.txt: -------------------------------------------------------------------------------- 1 | # A comment 2 | mypy==1.14.1 3 | types-jsonschema==4.23.0.20241208 4 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/pep_621_no_poetry_section_with_lock_file/poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /src/bin/migrate-to-uv.rs: -------------------------------------------------------------------------------- 1 | use migrate_to_uv::main as migrate_to_uv_main; 2 | 3 | fn main() { 4 | migrate_to_uv_main(); 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixtures/pip/existing_project/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "foobar" 3 | version = "1.0.0" 4 | requires-python = ">=3.13" 5 | -------------------------------------------------------------------------------- /tests/fixtures/pip_tools/full/requirements.in: -------------------------------------------------------------------------------- 1 | arrow 2 | httpx[cli,zstd]==0.28.1 3 | uvicorn @ git+https://github.com/encode/uvicorn 4 | -------------------------------------------------------------------------------- /tests/fixtures/pip_tools/existing_project/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "foobar" 3 | version = "1.0.0" 4 | requires-python = ">=3.13" 5 | -------------------------------------------------------------------------------- /tests/fixtures/pipenv/existing_project/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "foobar" 3 | version = "1.0.0" 4 | requires-python = ">=3.13" 5 | -------------------------------------------------------------------------------- /src/schema/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod hatch; 2 | pub mod pep_621; 3 | pub mod pipenv; 4 | pub mod poetry; 5 | pub mod pyproject; 6 | pub mod utils; 7 | pub mod uv; 8 | -------------------------------------------------------------------------------- /tests/fixtures/pip/full/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # This will be ignored 2 | -r requirements-typing.txt 3 | 4 | # A comment 5 | pytest==8.3.4 6 | ruff==0.8.4 7 | -------------------------------------------------------------------------------- /tests/fixtures/pip/existing_project/requirements.txt: -------------------------------------------------------------------------------- 1 | # A comment 2 | arrow==1.3.0 3 | httpx[cli]==0.28.1 4 | uvicorn @ git+https://github.com/encode/uvicorn 5 | -------------------------------------------------------------------------------- /tests/fixtures/pipenv/full/pyproject.toml: -------------------------------------------------------------------------------- 1 | # This comment should be preserved. 2 | [tool.ruff] 3 | fix = true 4 | 5 | [tool.ruff.format] 6 | preview = true 7 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/minimal/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "foobar" 3 | 4 | [tool.ruff] 5 | fix = true 6 | 7 | [tool.ruff.format] 8 | preview = true 9 | -------------------------------------------------------------------------------- /tests/fixtures/uv/with_lock/uv.lock: -------------------------------------------------------------------------------- 1 | # This is a uv lockfile. It is automatically generated by uv. 2 | # If you want to learn more, visit https://github.com/astral-sh/uv 3 | -------------------------------------------------------------------------------- /scripts/copy-changelog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | printf -- '---\n%s\n---\n%s\n' 'icon: lucide/scroll-text' "$(cat CHANGELOG.md)" > docs/CHANGELOG.md 6 | -------------------------------------------------------------------------------- /scripts/copy-contributing.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | printf -- '---\n%s\n---\n%s\n' 'icon: lucide/heart-handshake' "$(cat CONTRIBUTING.md)" > docs/CONTRIBUTING.md 6 | -------------------------------------------------------------------------------- /tests/fixtures/uv/with_lock/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.5,<2.0"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "test-project" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod cli; 2 | mod converters; 3 | mod detector; 4 | mod errors; 5 | mod logger; 6 | mod schema; 7 | mod toml; 8 | 9 | use crate::cli::cli; 10 | 11 | pub fn main() { 12 | cli(); 13 | } 14 | -------------------------------------------------------------------------------- /src/schema/utils.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Deserialize, Serialize, Eq, PartialEq)] 4 | #[serde(untagged)] 5 | pub enum SingleOrVec { 6 | Single(T), 7 | Vec(Vec), 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/uv/minimal/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.5,<2.0"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "test-project" 7 | version = "0.1.0" 8 | 9 | [tool.uv] 10 | package = false 11 | -------------------------------------------------------------------------------- /tests/fixtures/pipenv/with_lock_file/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | arrow = ">=1.2.3" 8 | 9 | [dev-packages] 10 | mypy = ">=1.13.0" 11 | 12 | [test] 13 | factory-boy = ">=3.2.1" 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{json5,yml,yaml}] 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /tests/fixtures/pipenv/existing_project/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | arrow = ">=1.2.3" 8 | 9 | [dev-packages] 10 | mypy = ">=1.13.0" 11 | 12 | [test] 13 | factory-boy = ">=3.2.1" 14 | 15 | [requires] 16 | python_version = "3.13" 17 | -------------------------------------------------------------------------------- /tests/fixtures/pip_tools/existing_project/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.13 3 | # by the following command: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | arrow==1.2.3 8 | # via -r requirements.in 9 | python-dateutil==2.7.0 10 | # via arrow 11 | six==1.15.0 12 | # via python-dateutil 13 | -------------------------------------------------------------------------------- /tests/fixtures/pip_tools/with_lock_file/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.13 3 | # by the following command: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | arrow==1.2.3 8 | # via -r requirements.in 9 | python-dateutil==2.7.0 10 | # via arrow 11 | six==1.15.0 12 | # via python-dateutil 13 | -------------------------------------------------------------------------------- /scripts/bump-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | version=$1 6 | 7 | sed -i "s/^version = \".*\"/version = \"${version}\"/" Cargo.toml pyproject.toml 8 | sed -i "s/^## Unreleased/## ${version} - $(date +%F)/" CHANGELOG.md 9 | ./scripts/copy-changelog.sh 10 | cargo update migrate-to-uv 11 | uv lock --upgrade-package migrate-to-uv 12 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "https://docs.renovatebot.com/renovate-schema.json", 3 | extends: [ 4 | "github>mkniewallner/renovate-config:default.json5", 5 | ":automergePatch", 6 | ], 7 | packageRules: [ 8 | { 9 | matchPackageNames: ["uv", "astral-sh/uv-pre-commit"], 10 | groupName: "uv-version", 11 | }, 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /tests/fixtures/pip_tools/with_lock_file/requirements-typing.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.13 3 | # by the following command: 4 | # 5 | # pip-compile requirements-typing.in 6 | # 7 | mypy==1.13.0 8 | # via -r requirements-typing.in 9 | mypy-extensions==1.0.0 10 | # via mypy 11 | typing-extensions==4.6.0 12 | # via 13 | # -c requirements-dev.txt 14 | # mypy 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test-unit 2 | test-unit: 3 | cargo test --lib 4 | 5 | .PHONY: test-integration 6 | test-integration: 7 | # Disable parallelism as this causes concurrency issues on Windows when uv cache is accessed. 8 | UV_NO_CACHE=1 cargo test --test '*' -- --test-threads 1 9 | 10 | .PHONY: test 11 | test: test-unit test-integration 12 | 13 | .PHONY: doc-serve 14 | doc-serve: 15 | uv run --only-group docs zensical serve 16 | -------------------------------------------------------------------------------- /tests/fixtures/pip_tools/full/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.13 3 | # by the following command: 4 | # 5 | # pip-compile requirements-dev.in 6 | # 7 | iniconfig==2.0.0 8 | # via pytest 9 | packaging==24.2 10 | # via pytest 11 | pluggy==1.5.0 12 | # via pytest 13 | pytest==8.3.4 14 | # via -r requirements-dev.in 15 | ruff==0.8.4 16 | # via -r requirements-dev.in 17 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/existing_project/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "foobar" 3 | version = "1.0.0" 4 | requires-python = ">=3.13" 5 | 6 | [tool.poetry] 7 | name = "foo" 8 | version = "0.0.1" 9 | description = "A description" 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.11" 13 | arrow = "^1.2.3" 14 | 15 | [tool.poetry.group.dev.dependencies] 16 | factory-boy = "^3.2.1" 17 | 18 | [tool.poetry.group.typing.dependencies] 19 | mypy = "^1.13.0" 20 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/with_lock_file/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | package-mode = false 3 | name = "foo" 4 | 5 | [tool.poetry.dependencies] 6 | python = "^3.11" 7 | arrow = "^1.2.3" 8 | 9 | [tool.poetry.group.dev.dependencies] 10 | factory-boy = "^3.2.1" 11 | 12 | [tool.poetry.group.typing.dependencies] 13 | mypy = "^1.13.0" 14 | 15 | [tool.poetry.group.profiling] 16 | optional = true 17 | 18 | [tool.poetry.group.profiling.dependencies] 19 | pyinstrument = "^5.0.2" 20 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/pep_621_no_poetry_section_with_lock_file/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.0.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [project] 6 | name = "foo" 7 | version = "0.1.0" 8 | description = "A fabulous project." 9 | requires-python = ">=3.11" 10 | dependencies = ["arrow>=1.2.3,<2"] 11 | 12 | [dependency-groups] 13 | dev = ["factory-boy>=3.2.1,<4"] 14 | typing = ["mypy>=1.13.0,<2"] 15 | profiling = ["pyinstrument>=5.0.2,<6"] 16 | -------------------------------------------------------------------------------- /src/converters/pipenv/project.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::pipenv::Requires; 2 | 3 | pub fn get_requires_python(pipenv_requires: Option) -> Option { 4 | let pipenv_requires = pipenv_requires?; 5 | 6 | if let Some(python_version) = pipenv_requires.python_version { 7 | return Some(format!("~={python_version}")); 8 | } 9 | 10 | if let Some(python_full_version) = pipenv_requires.python_full_version { 11 | return Some(format!("=={python_full_version}")); 12 | } 13 | 14 | None 15 | } 16 | -------------------------------------------------------------------------------- /tests/fixtures/pip_tools/with_lock_file/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.13 3 | # by the following command: 4 | # 5 | # pip-compile requirements-dev.in 6 | # 7 | factory-boy==3.2.1 8 | # via -r requirements-dev.in 9 | faker==33.1.0 10 | # via factory-boy 11 | python-dateutil==2.7.0 12 | # via 13 | # -c requirements.txt 14 | # faker 15 | six==1.15.0 16 | # via 17 | # -c requirements.txt 18 | # python-dateutil 19 | typing-extensions==4.6.0 20 | # via faker 21 | -------------------------------------------------------------------------------- /tests/fixtures/pip_tools/full/requirements-typing.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.13 3 | # by the following command: 4 | # 5 | # pip-compile requirements-typing.in 6 | # 7 | attrs==24.3.0 8 | # via referencing 9 | mypy==1.14.1 10 | # via -r requirements-typing.in 11 | mypy-extensions==1.0.0 12 | # via mypy 13 | referencing==0.35.1 14 | # via types-jsonschema 15 | rpds-py==0.22.3 16 | # via referencing 17 | types-jsonschema==4.23.0.20241208 18 | # via -r requirements-typing.in 19 | typing-extensions==4.12.2 20 | # via mypy 21 | -------------------------------------------------------------------------------- /src/converters/pipenv/sources.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::pipenv::Source; 2 | use crate::schema::uv::Index; 3 | 4 | pub fn get_indexes(pipenv_sources: Option>) -> Option> { 5 | Some( 6 | pipenv_sources? 7 | .iter() 8 | .map(|source| Index { 9 | name: source.name.clone(), 10 | url: Some(source.url.clone()), 11 | // https://pipenv.pypa.io/en/stable/indexes.html#index-restricted-packages 12 | explicit: (source.name.to_lowercase() != "pypi").then_some(true), 13 | ..Default::default() 14 | }) 15 | .collect(), 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /tests/fixtures/pip/full/requirements.txt: -------------------------------------------------------------------------------- 1 | # This will be ignored 2 | -c constraints.txt 3 | -cconstraints2.txt 4 | 5 | # A comment 6 | ## Another comment 7 | arrow==1.3.0 8 | httpx [ cli ] == 0.28.1 9 | uvicorn @ git+https://github.com/encode/uvicorn 10 | 11 | # Inline comments are not ignored, making parsing fail (https://github.com/mkniewallner/migrate-to-uv/issues/102) 12 | requests==2.32.3 # Inline comment 13 | 14 | # Non-PEP 508 compliant 15 | file:bar 16 | file:./bar 17 | -e file:bar 18 | -e file:./bar 19 | git+https://github.com/psf/requests 20 | git+https://github.com/psf/requests#egg=requests 21 | -e git+https://github.com/psf/requests#egg=requests 22 | -------------------------------------------------------------------------------- /src/schema/hatch.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Deserialize, Serialize)] 5 | pub struct Hatch { 6 | pub build: Option, 7 | } 8 | 9 | #[derive(Default, Eq, PartialEq, Deserialize, Serialize)] 10 | pub struct Build { 11 | pub targets: Option>, 12 | } 13 | 14 | #[derive(Default, Deserialize, Serialize, Eq, PartialEq)] 15 | pub struct BuildTarget { 16 | pub include: Option>, 17 | #[serde(rename = "force-include")] 18 | pub force_include: Option>, 19 | pub exclude: Option>, 20 | pub sources: Option>, 21 | } 22 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/with_migration_warnings/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | package-mode = false 3 | name = "foo" 4 | 5 | [tool.poetry.dependencies] 6 | python = "^3.11" 7 | arrow = "^1.2.3" 8 | 9 | [tool.poetry.group.dev.dependencies] 10 | factory-boy = "^3.2.1" 11 | 12 | [tool.poetry.group.typing.dependencies] 13 | mypy = "^1.13.0" 14 | 15 | [tool.poetry.group.profiling] 16 | optional = true 17 | 18 | [tool.poetry.group.profiling.dependencies] 19 | pyinstrument = "^5.0.2" 20 | 21 | [tool.poetry.extras] 22 | # Extra references a non-existing dependency. This is a recoverable error that will be shown as a warning. 23 | extra-with-non-existing-dependencies = ["non-existing-dependency"] 24 | -------------------------------------------------------------------------------- /src/logger.rs: -------------------------------------------------------------------------------- 1 | use clap_verbosity_flag::{InfoLevel, Verbosity}; 2 | use log::Level; 3 | use owo_colors::OwoColorize; 4 | use std::io::Write; 5 | 6 | pub fn configure(verbosity: Verbosity) { 7 | env_logger::Builder::new() 8 | .filter_level(verbosity.log_level_filter()) 9 | .format(|buf, record| match record.level() { 10 | Level::Error => writeln!(buf, "{}: {}", "error".red().bold(), record.args()), 11 | Level::Warn => writeln!(buf, "{}: {}", "warning".yellow().bold(), record.args()), 12 | Level::Debug => writeln!(buf, "{}: {}", "debug".blue().bold(), record.args()), 13 | _ => writeln!(buf, "{}", record.args()), 14 | }) 15 | .init(); 16 | } 17 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Mutex; 2 | 3 | pub struct MigrationError { 4 | pub error: String, 5 | pub recoverable: bool, 6 | } 7 | 8 | impl MigrationError { 9 | pub fn new(error: String, recoverable: bool) -> MigrationError { 10 | Self { error, recoverable } 11 | } 12 | } 13 | 14 | pub static MIGRATION_ERRORS: Mutex> = Mutex::new(Vec::new()); 15 | 16 | pub fn add_unrecoverable_error(error: String) { 17 | MIGRATION_ERRORS 18 | .lock() 19 | .unwrap() 20 | .push(MigrationError::new(error, false)); 21 | } 22 | 23 | pub fn add_recoverable_error(error: String) { 24 | MIGRATION_ERRORS 25 | .lock() 26 | .unwrap() 27 | .push(MigrationError::new(error, true)); 28 | } 29 | -------------------------------------------------------------------------------- /src/schema/pyproject.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::pep_621::Project; 2 | use crate::schema::pep_621::Tool; 3 | use indexmap::IndexMap; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Deserialize, Serialize)] 7 | pub struct PyProject { 8 | #[serde(rename = "build-system")] 9 | pub build_system: Option, 10 | pub project: Option, 11 | /// 12 | #[serde(rename = "dependency-groups")] 13 | pub dependency_groups: Option>>, 14 | pub tool: Option, 15 | } 16 | 17 | #[derive(Deserialize, Serialize)] 18 | #[serde(untagged)] 19 | pub enum DependencyGroupSpecification { 20 | String(String), 21 | Map { 22 | #[serde(rename = "include-group")] 23 | include_group: Option, 24 | }, 25 | } 26 | 27 | #[derive(Deserialize, Serialize)] 28 | pub struct BuildSystem { 29 | pub requires: Vec, 30 | #[serde(rename = "build-backend")] 31 | pub build_backend: Option, 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/validate-ci-workflows.yml: -------------------------------------------------------------------------------- 1 | name: Validate CI workflows 2 | 3 | on: 4 | pull_request: 5 | paths: [.github/workflows/*] 6 | push: 7 | branches: [main] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} 11 | cancel-in-progress: ${{ github.event_name == 'pull_request' }} 12 | 13 | env: 14 | # renovate: datasource=pypi depName=uv 15 | UV_VERSION: '0.9.18' 16 | # renovate: datasource=pypi depName=zizmor 17 | ZIZMOR_VERSION: '1.18.0' 18 | 19 | permissions: {} 20 | 21 | jobs: 22 | validate-ci-workflows: 23 | runs-on: ubuntu-24.04 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 27 | with: 28 | persist-credentials: false 29 | 30 | - name: Install uv 31 | uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7 32 | with: 33 | version: ${{ env.UV_VERSION }} 34 | 35 | - name: Run zizmor 36 | run: uvx zizmor@${{ env.ZIZMOR_VERSION }} . 37 | env: 38 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025, Mathieu Kniewallner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use insta_cmd::get_cargo_bin; 2 | use serde::Deserialize; 3 | use std::process::Command; 4 | 5 | macro_rules! apply_lock_filters { 6 | {} => { 7 | let mut settings = insta::Settings::clone_current(); 8 | settings.add_filter(r"Using .+", "Using [PYTHON_INTERPRETER]"); 9 | settings.add_filter(r"Defaulting to `\S+`", "Defaulting to `[PYTHON_VERSION]`"); 10 | settings.add_filter(r"Resolved \d+ packages in \S+", "Resolved [PACKAGES] packages in [TIME]"); 11 | settings.add_filter(r"Updated https://github.com/encode/uvicorn (\S+)", "Updated https://github.com/encode/uvicorn ([SHA1])"); 12 | let _bound = settings.bind_to_scope(); 13 | } 14 | } 15 | 16 | pub(crate) use apply_lock_filters; 17 | 18 | #[allow(dead_code)] 19 | #[derive(Deserialize, Eq, PartialEq, Debug)] 20 | pub struct UvLock { 21 | pub package: Option>, 22 | } 23 | 24 | #[allow(dead_code)] 25 | #[derive(Deserialize, Eq, PartialEq, Debug)] 26 | pub struct LockedPackage { 27 | pub name: String, 28 | pub version: String, 29 | } 30 | 31 | pub fn cli() -> Command { 32 | Command::new(get_cargo_bin("migrate-to-uv")) 33 | } 34 | -------------------------------------------------------------------------------- /tests/fixtures/pip_tools/full/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.13 3 | # by the following command: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | anyio==4.7.0 8 | # via httpx 9 | arrow==1.3.0 10 | # via -r requirements.in 11 | certifi==2024.12.14 12 | # via 13 | # httpcore 14 | # httpx 15 | click==8.1.8 16 | # via 17 | # httpx 18 | # uvicorn 19 | h11==0.14.0 20 | # via 21 | # httpcore 22 | # uvicorn 23 | httpcore==1.0.7 24 | # via httpx 25 | httpx==0.28.1 26 | # via -r requirements.in 27 | idna==3.10 28 | # via 29 | # anyio 30 | # httpx 31 | markdown-it-py==3.0.0 32 | # via rich 33 | mdurl==0.1.2 34 | # via markdown-it-py 35 | pygments==2.18.0 36 | # via 37 | # httpx 38 | # rich 39 | python-dateutil==2.9.0.post0 40 | # via arrow 41 | rich==13.9.4 42 | # via httpx 43 | six==1.17.0 44 | # via python-dateutil 45 | sniffio==1.3.1 46 | # via anyio 47 | types-python-dateutil==2.9.0.20241206 48 | # via arrow 49 | uvicorn @ git+https://github.com/encode/uvicorn 50 | # via -r requirements.in 51 | zstandard==0.23.0 52 | # via httpx 53 | -------------------------------------------------------------------------------- /.github/workflows/validate-renovate-config.yml: -------------------------------------------------------------------------------- 1 | name: Validate Renovate configuration 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - .github/workflows/validate-renovate-config.yml 7 | - .github/renovate.json5 8 | push: 9 | branches: [main] 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} 13 | cancel-in-progress: ${{ github.event_name == 'pull_request' }} 14 | 15 | permissions: {} 16 | 17 | jobs: 18 | validate-renovate-config: 19 | runs-on: ubuntu-24.04 20 | steps: 21 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 22 | with: 23 | persist-credentials: false 24 | 25 | - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 26 | with: 27 | # renovate: datasource=npm depName=pnpm 28 | version: '10.26.0' 29 | 30 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 31 | with: 32 | # renovate: datasource=node depName=node versioning=node 33 | node-version: '24' 34 | 35 | # Avoid installing packages that are newer than 1 week old to reduce supply chain attacks probability. 36 | - run: pnpm config set minimumReleaseAge 10080 37 | 38 | - run: pnpm --package renovate dlx renovate-config-validator 39 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "migrate-to-uv" 3 | version = "0.9.1" 4 | edition = "2024" 5 | rust-version = "1.92" 6 | license = "MIT" 7 | authors = ["Mathieu Kniewallner "] 8 | default-run = "migrate-to-uv" 9 | 10 | [dependencies] 11 | clap = { version = "=4.5.53", features = ["derive"] } 12 | clap-verbosity-flag = "=3.0.4" 13 | env_logger = "=0.11.8" 14 | indexmap = { version = "=2.12.1", features = ["serde"] } 15 | log = "=0.4.29" 16 | owo-colors = "=4.2.3" 17 | pep440_rs = "=0.7.3" 18 | pep508_rs = "=0.9.2" 19 | regex = "=1.12.2" 20 | serde = { version = "=1.0.228", features = ["derive"] } 21 | serde_json = "=1.0.145" 22 | toml = { version = "=0.9.8", features = ["preserve_order"] } 23 | toml_edit = { version = "=0.23.9", features = ["display", "serde"] } 24 | url = "=2.5.7" 25 | 26 | [dev-dependencies] 27 | dircpy = "=0.3.19" 28 | flate2 = "=1.1.5" 29 | insta = { version = "=1.44.3", features = ["filters"] } 30 | insta-cmd = "=0.6.0" 31 | rstest = "=0.26.1" 32 | tar = "=0.4.44" 33 | tempfile = "=3.23.0" 34 | walkdir = "=2.5.0" 35 | zip = "=6.0.0" 36 | 37 | [lints.clippy] 38 | pedantic = { level = "warn", priority = -1 } 39 | too_many_lines = "allow" 40 | 41 | [profile.dev.package] 42 | insta.opt-level = 3 43 | similar.opt-level = 3 44 | 45 | [profile.release] 46 | lto = "fat" 47 | codegen-units = 1 48 | panic = "abort" 49 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/pep_621/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.0.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [project] 6 | name = "foobar" 7 | version = "0.1.0" 8 | description = "A fabulous project." 9 | license = "MIT" 10 | authors = [{name = "John Doe", email = "john.doe@example.com"}] 11 | maintainers = [{name = "Dohn Joe", email = "dohn.joe@example.com"}] 12 | readme = "README.md" 13 | keywords = ["foo"] 14 | classifiers = ["Development Status :: 3 - Alpha"] 15 | requires-python = ">=3.11" 16 | dependencies = [ 17 | "arrow==1.2.3", 18 | "git-dep", 19 | "private-dep==3.4.5", 20 | ] 21 | 22 | [tool.poetry.dependencies] 23 | git-dep = { git = "https://example.com/foo/bar", tag = "v1.2.3" } 24 | private-dep = { source = "supplemental" } 25 | 26 | [tool.poetry.group.dev.dependencies] 27 | factory-boy = "^3.2.1" 28 | 29 | [tool.poetry.group.typing.dependencies] 30 | mypy = "^1.13.0" 31 | 32 | [tool.poetry.group.profiling] 33 | optional = true 34 | 35 | [tool.poetry.group.profiling.dependencies] 36 | pyinstrument = "^5.0.2" 37 | 38 | [[tool.poetry.source]] 39 | name = "PyPI" 40 | priority = "primary" 41 | 42 | [[tool.poetry.source]] 43 | name = "supplemental" 44 | url = "https://supplemental.example.com/simple/" 45 | priority = "supplemental" 46 | 47 | [tool.ruff] 48 | fix = true 49 | 50 | [tool.ruff.lint] 51 | # This comment should be preserved. 52 | fixable = ["I", "UP"] 53 | 54 | [tool.ruff.format] 55 | preview = true 56 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/with_migration_errors/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "foobar" 3 | # PEP 621 does not support multiple readme, this will abort the migration. 4 | readme = ["README.md", "README2.md"] 5 | 6 | [tool.poetry.dependencies] 7 | # PEP 621 does not support `||` operator, or `|` which are equivalent in Poetry, so this will abort the migration. 8 | caret-or = "^1.0||^2.0||^3.0" 9 | caret-or-single = "^1.0|^2.0|^3.0" 10 | caret-or-whitespaces = " ^1.0 || ^2.0 || ^3.0 " 11 | caret-or-mix-single-double-whitespaces = " ^1.0 | ^2.0 || ^3.0 " 12 | caret-or-and-pep-440 = "^1.0,<1.3||^2.0,<2.2" 13 | caret-or-table-version = { version = "^1.0||^2.0||^3.0" } 14 | caret-or-multiple-constraints = [ 15 | { python = ">=3.11", version = "^1.0||^2.0||^3.0" }, 16 | { python = "<3.11", version = "^1.0||^2.0" }, 17 | ] 18 | tilde-or = "~1.0||~2.0||~3.0" 19 | tilde-or-single = "~1.0|~2.0|~3.0" 20 | tilde-or-whitespaces = " ~1.0 || ~2.0 || ~3.0 " 21 | tilde-or-mix-single-double-whitespaces = " ~1.0 | ~2.0 || ~3.0 " 22 | tilde-or-and-pep-440 = "~1.0,<1.1||~1.0.1,<1.0.2" 23 | tilde-or-table-version = { version = "~1.0||~2.0||~3.0" } 24 | tilde-or-multiple-constraints = [ 25 | { python = ">=3.11", version = "~1.0||~2.0||~3.0" }, 26 | { python = "<3.11", version = "~1.0||~2.0" }, 27 | ] 28 | whitespace = ">=7.0 <7.1" 29 | whitespace-multiple = ">=7.0 <7.1" 30 | whitespace-caret = "^7.0 ^7.1" 31 | whitespace-caret-multiple = "^7.0 ^7.1" 32 | python-caret-or = { version = "1.2.3", python = "^3.11 || ^3.12" } 33 | python-caret-or-single = { version = "1.2.3", python = "^3.11 | ^3.12" } 34 | python-whitespace = { version = "1.2.3", python = "^3.11 <=3.14"} 35 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | # Switch to symlink once https://github.com/zensical/backlog/issues/55 is implemented. 3 | - repo: local 4 | hooks: 5 | - id: copy-changelog-docs 6 | name: copy changelog to docs 7 | entry: ./scripts/copy-changelog.sh 8 | language: system 9 | pass_filenames: false 10 | files: CHANGELOG\.md$ 11 | - id: copy-contributing-docs 12 | name: copy contributing to docs 13 | entry: ./scripts/copy-contributing.sh 14 | language: system 15 | pass_filenames: false 16 | files: CONTRIBUTING\.md$ 17 | 18 | - repo: local 19 | hooks: 20 | - id: cargo-check-lock 21 | name: check cargo lock file consistency 22 | entry: cargo check 23 | args: ["--locked", "--all-targets", "--all-features"] 24 | language: system 25 | pass_filenames: false 26 | files: Cargo\.toml$ 27 | 28 | - repo: local 29 | hooks: 30 | - id: cargo-fmt 31 | name: cargo fmt 32 | entry: cargo fmt 33 | args: ["--all", "--"] 34 | language: system 35 | types: [rust] 36 | pass_filenames: false 37 | 38 | - repo: local 39 | hooks: 40 | - id: cargo-clippy 41 | name: cargo clippy 42 | entry: cargo clippy 43 | args: ["--all-targets", "--all-features", "--", "-D", "warnings"] 44 | language: system 45 | types: [rust] 46 | pass_filenames: false 47 | 48 | - repo: https://github.com/astral-sh/uv-pre-commit 49 | rev: "0.9.18" 50 | hooks: 51 | - id: uv-lock 52 | name: check uv lock file consistency 53 | args: ["--locked"] 54 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv_incompatible/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.0.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "foobar" 7 | version = "0.1.0" 8 | description = "A fabulous project." 9 | authors = ["John Doe "] 10 | packages = [ 11 | { include = "packages_wheel", format = "wheel" }, 12 | { include = "packages_wheel_2", format = ["wheel"] }, 13 | { include = "packages_glob_sdist_wheel/**/*.py" }, 14 | { include = "packages_glob_sdist_wheel_2/**/*.py", format = ["sdist", "wheel"] }, 15 | { include = "packages_glob_wheel/**/*.py", format = "wheel" }, 16 | { include = "packages_glob_wheel_2/**/*.py", format = ["wheel"] }, 17 | { include = "packages_from_sdist_wheel", from = "from" }, 18 | { include = "packages_to_sdist_wheel", to = "to" }, 19 | { include = "packages_from_to_sdist_wheel", from = "from", to = "to" }, 20 | { include = "packages_glob_to_sdist_wheel/**/*.py", to = "to" }, 21 | { include = "packages_glob_from_to_sdist_wheel/**/*.py", from = "from", to = "to" }, 22 | { include = "text_file_sdist_wheel.txt" }, 23 | { include = "text_file_wheel.txt", format = "wheel" }, 24 | { include = "packages_without_init" }, 25 | { include = "packages_without_init_root" }, 26 | { include = "packages_from_without_init", from = "from" }, 27 | { include = "packages_from_without_init_root", from = "from" }, 28 | ] 29 | include = [ 30 | { path = "include_sdist_wheel", format = ["sdist", "wheel"] }, 31 | { path = "include_wheel", format = "wheel" }, 32 | { path = "include_wheel_2", format = ["wheel"] }, 33 | ] 34 | 35 | [tool.poetry.dependencies] 36 | python = ">=3.10" 37 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.5,<2.0"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "migrate-to-uv" 7 | version = "0.9.1" 8 | description = "" 9 | authors = [{ name = "Mathieu Kniewallner", email = "mathieu.kniewallner@gmail.com" }] 10 | requires-python = ">=3.8" 11 | license = "MIT" 12 | readme = "README.md" 13 | keywords = [ 14 | "uv", 15 | "migrate", 16 | "poetry", 17 | "pipenv", 18 | ] 19 | classifiers = [ 20 | "Development Status :: 3 - Alpha", 21 | "Environment :: Console", 22 | "Intended Audience :: Developers", 23 | "License :: OSI Approved :: MIT License", 24 | "Operating System :: OS Independent", 25 | "Programming Language :: Python", 26 | "Programming Language :: Python :: 3.8", 27 | "Programming Language :: Python :: 3.9", 28 | "Programming Language :: Python :: 3.10", 29 | "Programming Language :: Python :: 3.11", 30 | "Programming Language :: Python :: 3.12", 31 | "Programming Language :: Python :: 3.13", 32 | "Programming Language :: Python :: 3.14", 33 | "Programming Language :: Python :: 3 :: Only", 34 | "Programming Language :: Rust", 35 | "Topic :: Software Development :: Libraries", 36 | ] 37 | 38 | [project.urls] 39 | Documentation = "https://mkniewallner.github.io/migrate-to-uv/" 40 | Repository = "https://github.com/mkniewallner/migrate-to-uv" 41 | Changelog = "https://github.com/mkniewallner/migrate-to-uv/blob/main/CHANGELOG.md" 42 | Funding = "https://github.com/sponsors/mkniewallner" 43 | 44 | [dependency-groups] 45 | docs = ["zensical==0.0.11; python_version >= '3.10'"] 46 | 47 | [tool.maturin] 48 | bindings = "bin" 49 | module-name = "migrate_to_uv" 50 | 51 | [tool.uv] 52 | exclude-newer = "1 week" 53 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_uv/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.0.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "foobar" 7 | version = "0.1.0" 8 | description = "A fabulous project." 9 | authors = ["John Doe "] 10 | packages = [ 11 | { include = "packages_sdist_wheel" }, 12 | { include = "packages_sdist_wheel_2", format = ["sdist", "wheel"] }, 13 | { include = "packages_sdist", format = "sdist" }, 14 | { include = "packages_sdist_2", format = ["sdist"] }, 15 | # An empty array for `format` means that files are not included anywhere. 16 | { include = "packages_nowhere", format = [] }, 17 | { include = "packages_glob_sdist/**/*.py", format = "sdist" }, 18 | { include = "packages_glob_sdist_2/**/*.py", format = ["sdist"] }, 19 | # An empty array for `format` means that files are not included anywhere. 20 | { include = "packages_glob_nowhere/**/*.py", format = [] }, 21 | { include = "packages_sdist_wheel_with_excluded_files" }, 22 | { include = "text_file_sdist.txt", format = "sdist" }, 23 | { include = "FILE_WITHOUT_EXTENSION_SDIST", format = "sdist" }, 24 | ] 25 | include = [ 26 | "include_sdist", 27 | { path = "include_sdist_2" }, 28 | { path = "include_sdist_3", format = "sdist" }, 29 | { path = "include_sdist_4", format = ["sdist"] }, 30 | # An empty array for `format` means that files are not included anywhere. 31 | { path = "include_nowhere", format = [] }, 32 | { path = "INCLUDE_FILE_WITHOUT_EXTENSION_SDIST", format = "sdist" }, 33 | ] 34 | exclude = [ 35 | "packages_sdist_wheel_with_excluded_files/bar.py", 36 | "packages_sdist_wheel_with_excluded_files/foobar", 37 | ] 38 | 39 | [tool.poetry.dependencies] 40 | python = ">=3.10" 41 | 42 | -------------------------------------------------------------------------------- /src/schema/pep_621.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::poetry::Poetry; 2 | use crate::schema::uv::Uv; 3 | use indexmap::IndexMap; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_json::Value; 6 | use std::collections::HashMap; 7 | 8 | /// 9 | #[derive(Default, Deserialize, Serialize)] 10 | pub struct Project { 11 | pub name: Option, 12 | pub version: Option, 13 | pub description: Option, 14 | pub authors: Option>, 15 | #[serde(rename = "requires-python")] 16 | pub requires_python: Option, 17 | pub readme: Option, 18 | pub license: Option, 19 | pub maintainers: Option>, 20 | pub keywords: Option>, 21 | pub classifiers: Option>, 22 | pub dependencies: Option>, 23 | #[serde(rename = "optional-dependencies")] 24 | pub optional_dependencies: Option>>, 25 | pub urls: Option>, 26 | pub scripts: Option>, 27 | #[serde(rename = "gui-scripts")] 28 | pub gui_scripts: Option>, 29 | #[serde(rename = "entry-points")] 30 | pub entry_points: Option>>, 31 | #[serde(flatten)] 32 | pub remaining_fields: HashMap, 33 | } 34 | 35 | #[derive(Deserialize, Serialize)] 36 | pub struct AuthorOrMaintainer { 37 | pub name: Option, 38 | pub email: Option, 39 | } 40 | 41 | #[derive(Deserialize, Serialize)] 42 | #[serde(untagged)] 43 | pub enum License { 44 | String(String), 45 | Map { 46 | text: Option, 47 | file: Option, 48 | }, 49 | } 50 | 51 | #[derive(Deserialize, Serialize, Default)] 52 | pub struct Tool { 53 | pub poetry: Option, 54 | pub uv: Option, 55 | } 56 | -------------------------------------------------------------------------------- /src/converters/pip/dependencies.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::add_recoverable_error; 2 | use owo_colors::OwoColorize; 3 | use pep508_rs::Requirement; 4 | use std::fs; 5 | use std::path::Path; 6 | use std::str::FromStr; 7 | use url::Url; 8 | 9 | pub fn get(project_path: &Path, requirements_files: Vec) -> Option> { 10 | let mut dependencies: Vec = Vec::new(); 11 | 12 | for requirements_file in requirements_files { 13 | let requirements_content = 14 | fs::read_to_string(project_path.join(requirements_file.clone())).unwrap(); 15 | 16 | for line in requirements_content.lines() { 17 | let line = line.trim(); 18 | 19 | // Ignore empty lines, comments. Also ignore lines starting with `-` to ignore arguments 20 | // (package names cannot start with a hyphen). No argument is supported yet, so we can 21 | // simply ignore all of them. 22 | if line.is_empty() || line.starts_with('#') || line.starts_with('-') { 23 | continue; 24 | } 25 | 26 | let dependency = match line.split_once(" #") { 27 | Some((dependency, _)) => dependency, 28 | None => line, 29 | }; 30 | 31 | let dependency_specification = Requirement::::from_str(dependency); 32 | 33 | if let Ok(dependency_specification) = dependency_specification { 34 | dependencies.push(dependency_specification.to_string()); 35 | } else { 36 | add_recoverable_error(format!( 37 | "\"{}\" from \"{}\" could not be automatically migrated, try running \"{}\".", 38 | dependency.bold(), 39 | requirements_file.bold(), 40 | format!("uv add {dependency}").bold(), 41 | )); 42 | } 43 | } 44 | } 45 | 46 | if dependencies.is_empty() { 47 | return None; 48 | } 49 | Some(dependencies) 50 | } 51 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: migrate-to-uv 2 | edit_uri: edit/main/docs/ 3 | repo_name: mkniewallner/migrate-to-uv 4 | repo_url: https://github.com/mkniewallner/migrate-to-uv 5 | site_url: https://mkniewallner.github.io/migrate-to-uv 6 | site_description: Migrate to uv from another package manager. 7 | site_author: Mathieu Kniewallner 8 | 9 | nav: 10 | - Introduction: index.md 11 | - Usage: usage.md 12 | - Supported package managers: supported-package-managers.md 13 | - Configuration: configuration.md 14 | - Changelog: CHANGELOG.md 15 | - Contributing: CONTRIBUTING.md 16 | 17 | plugins: 18 | - search 19 | 20 | theme: 21 | name: material 22 | features: 23 | - content.action.edit 24 | - content.code.copy 25 | - navigation.footer 26 | palette: 27 | - media: "(prefers-color-scheme)" 28 | toggle: 29 | icon: material/brightness-auto 30 | name: Switch to light mode 31 | - media: "(prefers-color-scheme: light)" 32 | scheme: default 33 | primary: deep purple 34 | accent: deep orange 35 | toggle: 36 | icon: material/brightness-7 37 | name: Switch to dark mode 38 | - media: "(prefers-color-scheme: dark)" 39 | scheme: slate 40 | primary: deep purple 41 | accent: deep orange 42 | toggle: 43 | icon: material/brightness-4 44 | name: Switch to system preferences 45 | icon: 46 | logo: octicons/package-dependents-24 47 | repo: fontawesome/brands/github 48 | 49 | extra: 50 | social: 51 | - icon: fontawesome/brands/github 52 | link: https://github.com/mkniewallner/migrate-to-uv 53 | - icon: fontawesome/brands/python 54 | link: https://pypi.org/project/migrate-to-uv/ 55 | 56 | markdown_extensions: 57 | - admonition 58 | - attr_list 59 | - md_in_html 60 | - pymdownx.details 61 | - pymdownx.superfences 62 | - toc: 63 | permalink: true 64 | - pymdownx.arithmatex: 65 | generic: true 66 | 67 | validation: 68 | omitted_files: warn 69 | absolute_links: warn 70 | unrecognized_links: warn 71 | anchors: warn 72 | -------------------------------------------------------------------------------- /tests/fixtures/pipenv/full/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [[source]] 7 | url = "https://example.com/simple" 8 | verify_ssl = true 9 | name = "other-index" 10 | 11 | [packages] 12 | dep = "==1.2.3" 13 | dep-2 = "1.2.3" 14 | dep-3 = ">=1.2.3" 15 | dep-4 = "~=1.2.3" 16 | dep-5 = "~=1.2" 17 | dep-star = "*" 18 | 19 | # Tables 20 | with-version-only = { version = "==1.2.3" } 21 | with-version-only-2 = { version = "1.2.3" } 22 | with-version-only-star = { version = "*" } 23 | with-extras = { version = "==1.2.3", extras = ["foo", "bar"] } 24 | with-source = { version = "==1.2.3", index = "other-index" } 25 | 26 | # Path 27 | local-package = { path = "package/" } 28 | local-package-2 = { path = "another-package/", editable = false } 29 | local-package-editable = { path = "package/dist/package-0.1.0.tar.gz", editable = true } 30 | 31 | # Git 32 | git = { git = "https://example.com/foo/bar.git" } 33 | git-ref = { git = "https://example.com/foo/bar.git", ref = "v1.2.3" } 34 | 35 | # Markers 36 | markers = { version = "==1.2.3", markers = "sys_platform == 'win32'" } 37 | markers-2 = { version = "==1.2.3", markers = "sys_platform == 'win32'", os_name= "== 'nt'", sys_platform = "!= 'darwin'", platform_machine = "== 'x86_64'", platform_python_implementation = "== 'CPython'", platform_release = "== '1.2.3'", platform_system = "== 'Windows'", platform_version = "== '1.2.3'", python_version = "> '3.8'", python_full_version = "> '3.8.0'", implementation_name = "!= 'pypy'", implementation_version = "> '3.8'", additional_key = "foobar" } 38 | 39 | [dev-packages] 40 | dev-package = "==1.2.3" 41 | dev-package-local = { path = "package" } 42 | dev-package-source = { path = "package", index = "other-index" } 43 | 44 | [packages-category] 45 | category-package = "==1.2.3" 46 | category-package-2 = { version = "==1.2.3", index = "other-index" } 47 | 48 | [packages-category-2] 49 | category-2-package = { version = "==1.2.3", index = "other-index" } 50 | category-2-package-2 = { git = "https://example.com/foo/bar.git", ref = "v1.2.3", markers = "sys_platform == 'win32'" } 51 | 52 | [requires] 53 | python_version = "3.13" 54 | python_full_version = "3.13.1" 55 | 56 | [pipenv] 57 | allow_prereleases = true 58 | install_search_all_sources = true 59 | extra-key = "bar" 60 | 61 | [scripts] 62 | "foo" = "bar:run" 63 | -------------------------------------------------------------------------------- /src/converters/poetry/sources.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::poetry::{DependencySpecification, Source, SourcePriority}; 2 | use crate::schema::uv::{Index, SourceIndex}; 3 | 4 | pub fn get_source_index(dependency_specification: &DependencySpecification) -> Option { 5 | match dependency_specification { 6 | DependencySpecification::Map { 7 | source: Some(source), 8 | .. 9 | } => Some(SourceIndex { 10 | index: Some(source.clone()), 11 | ..Default::default() 12 | }), 13 | DependencySpecification::Map { url: Some(url), .. } => Some(SourceIndex { 14 | url: Some(url.clone()), 15 | ..Default::default() 16 | }), 17 | DependencySpecification::Map { 18 | path: Some(path), 19 | develop, 20 | .. 21 | } => Some(SourceIndex { 22 | path: Some(path.clone()), 23 | editable: *develop, 24 | ..Default::default() 25 | }), 26 | DependencySpecification::Map { 27 | git: Some(git), 28 | branch, 29 | rev, 30 | tag, 31 | subdirectory, 32 | .. 33 | } => Some(SourceIndex { 34 | git: Some(git.clone()), 35 | branch: branch.clone(), 36 | rev: rev.clone(), 37 | tag: tag.clone(), 38 | subdirectory: subdirectory.clone(), 39 | ..Default::default() 40 | }), 41 | _ => None, 42 | } 43 | } 44 | 45 | pub fn get_indexes(poetry_sources: Option>) -> Option> { 46 | Some( 47 | poetry_sources? 48 | .iter() 49 | .map(|source| Index { 50 | name: source.name.clone(), 51 | url: match source.name.to_lowercase().as_str() { 52 | "pypi" => Some("https://pypi.org/simple/".to_string()), 53 | _ => source.url.clone(), 54 | }, 55 | default: match source.priority { 56 | Some(SourcePriority::Default | SourcePriority::Primary) => Some(true), 57 | _ => None, 58 | }, 59 | explicit: match source.priority { 60 | Some(SourcePriority::Explicit) => Some(true), 61 | _ => None, 62 | }, 63 | }) 64 | .collect(), 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # migrate-to-uv 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/migrate-to-uv.svg)](https://pypi.org/project/migrate-to-uv/) 4 | [![License](https://img.shields.io/pypi/l/migrate-to-uv.svg)](https://pypi.org/project/migrate-to-uv/) 5 | [![Supported Python versions](https://img.shields.io/pypi/pyversions/migrate-to-uv.svg)](https://pypi.org/project/migrate-to-uv/) 6 | 7 | `migrate-to-uv` migrates a project to [uv](https://github.com/astral-sh/uv) from another package manager. 8 | 9 | ## Usage 10 | 11 | ```bash 12 | # With uv 13 | uvx migrate-to-uv 14 | 15 | # With pipx 16 | pipx run migrate-to-uv 17 | ``` 18 | 19 | ## Supported package managers 20 | 21 | The following package managers are supported: 22 | 23 | - [Poetry](https://python-poetry.org/) (including projects 24 | using [PEP 621 in Poetry 2.0+](https://python-poetry.org/blog/announcing-poetry-2.0.0/)) 25 | - [Pipenv](https://pipenv.pypa.io/en/stable/) 26 | - [pip-tools](https://pip-tools.readthedocs.io/en/stable/) 27 | - [pip](https://pip.pypa.io/en/stable/) 28 | 29 | More package managers (e.g., [setuptools](https://setuptools.pypa.io/en/stable/)) could be implemented in the future. 30 | 31 | ## Features 32 | 33 | `migrate-to-uv` converts most existing metadata from supported package managers when migrating to uv, including: 34 | 35 | - [Project metadata](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#writing-pyproject-toml) (`name`, `version`, `authors`, ...) 36 | - [Dependencies and optional dependencies](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#dependencies-optional-dependencies) 37 | - [Dependency groups](https://packaging.python.org/en/latest/specifications/dependency-groups/#dependency-groups) 38 | - [Dependency sources](https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-sources) (index, git, URL, path) 39 | - [Dependency markers](https://packaging.python.org/en/latest/specifications/dependency-specifiers/) 40 | - [Entry points](https://packaging.python.org/en/latest/specifications/pyproject-toml/#entry-points) 41 | 42 | Version definitions set for dependencies are also preserved, and converted to their 43 | equivalent [PEP 440](https://peps.python.org/pep-0440/) for package managers that use their own syntax (for instance 44 | Poetry's [caret](https://python-poetry.org/docs/dependency-specification/#caret-requirements) syntax). 45 | 46 | At the end of the migration, `migrate-to-uv` also generates `uv.lock` file with `uv lock` command to lock dependencies, 47 | and keeps dependencies (both direct and transitive) to the exact same versions they were locked to with the previous 48 | package manager, if a lock file was found. 49 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Setup 4 | 5 | [Rust](https://rustup.rs/) is required to build the project. 6 | 7 | ## Linting and formatting 8 | 9 | The project uses several tools from the Rust ecosystem to check for common linting issues, and ensure that the code is 10 | correctly formatted, like: 11 | 12 | - [clippy](https://doc.rust-lang.org/clippy/) for linting 13 | - [rustfmt](https://rust-lang.github.io/rustfmt/) for formatting 14 | 15 | [pre-commit](https://pre-commit.com/) is used to ensure that all tools are run at commit time. You can install hooks in 16 | the project with: 17 | 18 | ```bash 19 | pre-commit install 20 | ``` 21 | 22 | This will automatically run the relevant git hooks based on the files that are modified whenever you commit. 23 | 24 | You can also run all hooks manually without committing with: 25 | 26 | ```bash 27 | pre-commit run --all-files 28 | ``` 29 | 30 | ## Testing 31 | 32 | Both unit and integration tests are used to ensure that the code work as intended. They can be run with: 33 | 34 | ```bash 35 | make test 36 | ``` 37 | 38 | Unit tests are located in modules, alongside the code, under `src` directory, and can be run with: 39 | 40 | ```bash 41 | make test-unit 42 | ``` 43 | 44 | Integration tests are located under `tests` directory, and can be run with: 45 | 46 | ```bash 47 | make test-integration 48 | ``` 49 | 50 | As integration tests depend on [uv](https://docs.astral.sh/uv/) for performing locking, make sure that it is present on 51 | your machine before running them. 52 | 53 | ### Snapshots 54 | 55 | Both unit and integration tests use snapshot testing through [insta](https://insta.rs/), to assert things like the 56 | content of files or command line outputs. Those snapshots can either be asserted right into the code, or against files 57 | stored in `snapshots` directories, for instance: 58 | 59 | ```rust 60 | #[test] 61 | fn test_with_snapshots() { 62 | // Inline snapshot 63 | insta::assert_snapshot!(foo(), @r###" 64 | [project] 65 | name = "foo" 66 | version = "0.0.1" 67 | "###); 68 | 69 | // External snapshot, stored under `snapshots` directory 70 | insta::assert_snapshot!(foo()); 71 | } 72 | ``` 73 | 74 | In both cases, if you update code that changes the output of snapshots, you will be prompted to review the updated 75 | snapshots with: 76 | 77 | ```bash 78 | cargo insta review 79 | ``` 80 | 81 | You can then accept the changes, if they look correct according to the changed code. 82 | 83 | ## Documentation 84 | 85 | Documentation is built using [zensical](https://zensical.org). 86 | 87 | It can be run locally with [uv](https://docs.astral.sh/uv/) by using: 88 | 89 | ```bash 90 | make doc-serve 91 | ``` 92 | -------------------------------------------------------------------------------- /tests/fixtures/poetry/build_backend_hatch/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.0.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "foobar" 7 | version = "0.1.0" 8 | description = "A fabulous project." 9 | authors = ["John Doe "] 10 | packages = [ 11 | { include = "packages_sdist_wheel" }, 12 | { include = "packages_sdist_wheel_2", format = ["sdist", "wheel"] }, 13 | { include = "packages_sdist", format = "sdist" }, 14 | { include = "packages_sdist_2", format = ["sdist"] }, 15 | { include = "packages_wheel", format = "wheel" }, 16 | { include = "packages_wheel_2", format = ["wheel"] }, 17 | # An empty array for `format` means that files are not included anywhere. 18 | { include = "packages_nowhere", format = [] }, 19 | { include = "packages_glob_sdist_wheel/**/*.py" }, 20 | { include = "packages_glob_sdist_wheel_2/**/*.py", format = ["sdist", "wheel"] }, 21 | { include = "packages_glob_sdist/**/*.py", format = "sdist" }, 22 | { include = "packages_glob_sdist_2/**/*.py", format = ["sdist"] }, 23 | { include = "packages_glob_wheel/**/*.py", format = "wheel" }, 24 | { include = "packages_glob_wheel_2/**/*.py", format = ["wheel"] }, 25 | # An empty array for `format` means that files are not included anywhere. 26 | { include = "packages_glob_nowhere/**/*.py", format = [] }, 27 | { include = "packages_from_sdist_wheel", from = "from" }, 28 | { include = "packages_to_sdist_wheel", to = "to" }, 29 | { include = "packages_from_to_sdist_wheel", from = "from", to = "to" }, 30 | { include = "packages_glob_to_sdist_wheel/**/*.py", to = "to" }, 31 | { include = "packages_glob_from_to_sdist_wheel/**/*.py", from = "from", to = "to" }, 32 | { include = "packages_sdist_wheel_with_excluded_files" }, 33 | { include = "text_file_sdist_wheel.txt" }, 34 | { include = "text_file_sdist.txt", format = "sdist" }, 35 | { include = "text_file_wheel.txt", format = "wheel" }, 36 | ] 37 | include = [ 38 | "include_sdist", 39 | { path = "include_sdist_2" }, 40 | { path = "include_sdist_3", format = "sdist" }, 41 | { path = "include_sdist_4", format = ["sdist"] }, 42 | { path = "include_sdist_wheel", format = ["sdist", "wheel"] }, 43 | { path = "include_wheel", format = "wheel" }, 44 | { path = "include_wheel_2", format = ["wheel"] }, 45 | # An empty array for `format` means that files are not included anywhere. 46 | { path = "include_nowhere", format = [] }, 47 | ] 48 | exclude = [ 49 | "packages_sdist_wheel_with_excluded_files/bar.py", 50 | "packages_sdist_wheel_with_excluded_files/foobar", 51 | ] 52 | 53 | [tool.poetry.dependencies] 54 | python = ">=3.10" 55 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | --- 2 | icon: lucide/heart-handshake 3 | --- 4 | # Contributing 5 | 6 | ## Setup 7 | 8 | [Rust](https://rustup.rs/) is required to build the project. 9 | 10 | ## Linting and formatting 11 | 12 | The project uses several tools from the Rust ecosystem to check for common linting issues, and ensure that the code is 13 | correctly formatted, like: 14 | 15 | - [clippy](https://doc.rust-lang.org/clippy/) for linting 16 | - [rustfmt](https://rust-lang.github.io/rustfmt/) for formatting 17 | 18 | [pre-commit](https://pre-commit.com/) is used to ensure that all tools are run at commit time. You can install hooks in 19 | the project with: 20 | 21 | ```bash 22 | pre-commit install 23 | ``` 24 | 25 | This will automatically run the relevant git hooks based on the files that are modified whenever you commit. 26 | 27 | You can also run all hooks manually without committing with: 28 | 29 | ```bash 30 | pre-commit run --all-files 31 | ``` 32 | 33 | ## Testing 34 | 35 | Both unit and integration tests are used to ensure that the code work as intended. They can be run with: 36 | 37 | ```bash 38 | make test 39 | ``` 40 | 41 | Unit tests are located in modules, alongside the code, under `src` directory, and can be run with: 42 | 43 | ```bash 44 | make test-unit 45 | ``` 46 | 47 | Integration tests are located under `tests` directory, and can be run with: 48 | 49 | ```bash 50 | make test-integration 51 | ``` 52 | 53 | As integration tests depend on [uv](https://docs.astral.sh/uv/) for performing locking, make sure that it is present on 54 | your machine before running them. 55 | 56 | ### Snapshots 57 | 58 | Both unit and integration tests use snapshot testing through [insta](https://insta.rs/), to assert things like the 59 | content of files or command line outputs. Those snapshots can either be asserted right into the code, or against files 60 | stored in `snapshots` directories, for instance: 61 | 62 | ```rust 63 | #[test] 64 | fn test_with_snapshots() { 65 | // Inline snapshot 66 | insta::assert_snapshot!(foo(), @r###" 67 | [project] 68 | name = "foo" 69 | version = "0.0.1" 70 | "###); 71 | 72 | // External snapshot, stored under `snapshots` directory 73 | insta::assert_snapshot!(foo()); 74 | } 75 | ``` 76 | 77 | In both cases, if you update code that changes the output of snapshots, you will be prompted to review the updated 78 | snapshots with: 79 | 80 | ```bash 81 | cargo insta review 82 | ``` 83 | 84 | You can then accept the changes, if they look correct according to the changed code. 85 | 86 | ## Documentation 87 | 88 | Documentation is built using [zensical](https://zensical.org). 89 | 90 | It can be run locally with [uv](https://docs.astral.sh/uv/) by using: 91 | 92 | ```bash 93 | make doc-serve 94 | ``` 95 | -------------------------------------------------------------------------------- /src/schema/uv.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::utils::SingleOrVec; 2 | use indexmap::IndexMap; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Default, Deserialize, Serialize, Eq, PartialEq)] 6 | pub struct Uv { 7 | pub package: Option, 8 | /// 9 | pub index: Option>, 10 | /// 11 | pub sources: Option>, 12 | /// 13 | #[serde(rename = "default-groups")] 14 | pub default_groups: Option>, 15 | #[serde(rename = "constraint-dependencies")] 16 | pub constraint_dependencies: Option>, 17 | #[serde(rename = "build-backend")] 18 | pub build_backend: Option, 19 | } 20 | 21 | #[derive(Default, Deserialize, Serialize, Eq, PartialEq)] 22 | pub struct Index { 23 | pub name: String, 24 | pub url: Option, 25 | pub default: Option, 26 | pub explicit: Option, 27 | } 28 | 29 | #[derive(Default, Deserialize, Serialize, Eq, PartialEq)] 30 | pub struct SourceIndex { 31 | pub index: Option, 32 | pub path: Option, 33 | pub editable: Option, 34 | pub git: Option, 35 | pub tag: Option, 36 | pub branch: Option, 37 | pub rev: Option, 38 | pub subdirectory: Option, 39 | pub url: Option, 40 | pub marker: Option, 41 | } 42 | 43 | #[derive(Deserialize, Serialize, Eq, PartialEq)] 44 | #[serde(untagged)] 45 | pub enum SourceContainer { 46 | SourceIndex(SourceIndex), 47 | SourceIndexes(Vec), 48 | } 49 | 50 | #[derive(Default, Deserialize, Serialize, Eq, PartialEq)] 51 | pub struct UvBuildBackend { 52 | #[serde(rename = "module-name")] 53 | pub module_name: Option>, 54 | #[serde(rename = "module-root")] 55 | pub module_root: Option, 56 | pub namespace: Option, 57 | pub data: Option, 58 | #[serde(rename = "default-excludes")] 59 | pub default_excludes: Option, 60 | #[serde(rename = "source-exclude")] 61 | pub source_exclude: Option>, 62 | #[serde(rename = "source-include")] 63 | pub source_include: Option>, 64 | #[serde(rename = "wheel-exclude")] 65 | pub wheel_exclude: Option>, 66 | } 67 | 68 | #[derive(Default, Deserialize, Serialize, Eq, PartialEq)] 69 | pub struct UvBuildBackendData { 70 | data: Option, 71 | headers: Option, 72 | platlib: Option, 73 | purelib: Option, 74 | scripts: Option, 75 | } 76 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | icon: lucide/house 3 | --- 4 | # Introduction 5 | 6 | `migrate-to-uv` migrates a project to [uv](https://github.com/astral-sh/uv) from another package manager. 7 | 8 | Try it now: 9 | 10 | ```bash 11 | # With uv 12 | uvx migrate-to-uv 13 | 14 | # With pipx 15 | pipx run migrate-to-uv 16 | ``` 17 | 18 | The following package managers are supported: 19 | 20 | - [Poetry](supported-package-managers.md#poetry) (including projects 21 | using [PEP 621 in Poetry 2.0+](https://python-poetry.org/blog/announcing-poetry-2.0.0/)) 22 | - [Pipenv](supported-package-managers.md#pipenv) 23 | - [pip-tools](supported-package-managers.md#pip-tools) 24 | - [pip](supported-package-managers.md#pip) 25 | 26 | More package managers (e.g., [setuptools](https://setuptools.pypa.io/en/stable/)) could be implemented in the 27 | future. 28 | 29 | !!! warning 30 | 31 | Although `migrate-to-uv` matches current package manager definition as closely as possible when performing the migration, it is still heavily recommended to double check the end result, especially if you are migrating a package that is meant to be publicly distributed. 32 | 33 | If you notice a behaviour that does not match the previous package manager when migrating, please [raise an issue](https://github.com/mkniewallner/migrate-to-uv/issues), if not already reported. 34 | 35 | ## Features 36 | 37 | `migrate-to-uv` converts most existing metadata from supported package managers when migrating to uv, including: 38 | 39 | - [Project metadata](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#writing-pyproject-toml) (`name`, `version`, `authors`, ...) 40 | - [Dependencies and optional dependencies](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#dependencies-optional-dependencies) 41 | - [Dependency groups](https://packaging.python.org/en/latest/specifications/dependency-groups/#dependency-groups) 42 | - [Dependency sources](https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-sources) (index, git, URL, path) 43 | - [Dependency markers](https://packaging.python.org/en/latest/specifications/dependency-specifiers/) 44 | - [Entry points](https://packaging.python.org/en/latest/specifications/pyproject-toml/#entry-points) 45 | 46 | Version definitions set for dependencies are also preserved, and converted to their 47 | equivalent [PEP 440](https://peps.python.org/pep-0440/) for package managers that use their own syntax (for instance 48 | [caret](https://python-poetry.org/docs/dependency-specification/#caret-requirements) for Poetry). 49 | 50 | At the end of the migration, `migrate-to-uv` also generates `uv.lock` file with `uv lock` command to lock dependencies, 51 | and keeps dependencies (both direct and transitive) to the exact same versions they were locked to with the previous 52 | package manager, if a lock file was found. 53 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} 10 | cancel-in-progress: ${{ github.event_name == 'pull_request' }} 11 | 12 | env: 13 | PYTHON_VERSION: '3.14' 14 | # renovate: datasource=pypi depName=uv 15 | UV_VERSION: '0.9.18' 16 | 17 | permissions: {} 18 | 19 | jobs: 20 | quality: 21 | runs-on: ubuntu-24.04 22 | steps: 23 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 24 | with: 25 | persist-credentials: false 26 | 27 | - name: Install Rust toolchain 28 | run: rustup component add clippy rustfmt 29 | 30 | - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 31 | 32 | - name: Run cargo fmt 33 | run: cargo fmt --all --check 34 | 35 | - name: Run clippy 36 | run: cargo clippy --all-targets --all-features -- -D warnings 37 | 38 | tests: 39 | strategy: 40 | matrix: 41 | os: 42 | - name: linux 43 | image: ubuntu-24.04 44 | - name: macos 45 | image: macos-26 46 | - name: windows 47 | image: windows-2025 48 | fail-fast: false 49 | runs-on: ${{ matrix.os.image }} 50 | name: tests (${{ matrix.os.name }}) 51 | steps: 52 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 53 | with: 54 | persist-credentials: false 55 | 56 | - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 57 | 58 | - name: Install uv 59 | uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7 60 | with: 61 | version: ${{ env.UV_VERSION }} 62 | 63 | - name: Install Python 64 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6 65 | with: 66 | python-version: ${{ env.PYTHON_VERSION }} 67 | 68 | - name: Run unit tests 69 | run: make test-unit 70 | 71 | - name: Run integration tests 72 | run: make test-integration 73 | 74 | check-docs: 75 | runs-on: ubuntu-24.04 76 | steps: 77 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 78 | with: 79 | persist-credentials: false 80 | 81 | - name: Install uv 82 | uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7 83 | with: 84 | version: ${{ env.UV_VERSION }} 85 | 86 | - name: Install Python 87 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6 88 | with: 89 | python-version: ${{ env.PYTHON_VERSION }} 90 | 91 | - name: Check if documentation can be built 92 | run: uv run --only-group docs zensical build --strict 93 | -------------------------------------------------------------------------------- /src/schema/pipenv.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde::Deserialize; 3 | use std::collections::BTreeMap; 4 | 5 | #[derive(Deserialize)] 6 | pub struct Pipfile { 7 | pub source: Option>, 8 | pub packages: Option>, 9 | #[serde(rename = "dev-packages")] 10 | pub dev_packages: Option>, 11 | pub requires: Option, 12 | /// Not used, this avoids having the section in `category_groups` below. 13 | #[allow(dead_code)] 14 | pipenv: Option, 15 | /// Not used, this avoids having the section in `category_groups` below 16 | #[allow(dead_code)] 17 | scripts: Option, 18 | /// Assume that the remaining keys are category groups (). 19 | #[serde(flatten)] 20 | pub category_groups: Option>>, 21 | } 22 | 23 | #[derive(Deserialize)] 24 | #[serde(untagged)] 25 | #[allow(clippy::large_enum_variant)] 26 | pub enum DependencySpecification { 27 | String(String), 28 | Map { 29 | version: Option, 30 | extras: Option>, 31 | markers: Option, 32 | index: Option, 33 | git: Option, 34 | #[serde(rename = "ref")] 35 | ref_: Option, 36 | path: Option, 37 | editable: Option, 38 | #[serde(flatten)] 39 | keyword_markers: KeywordMarkers, 40 | }, 41 | } 42 | 43 | #[derive(Deserialize)] 44 | pub struct Source { 45 | pub name: String, 46 | pub url: String, 47 | } 48 | 49 | #[derive(Deserialize)] 50 | pub struct Requires { 51 | pub python_version: Option, 52 | pub python_full_version: Option, 53 | } 54 | 55 | /// Markers can be set as keywords: 56 | #[derive(Deserialize)] 57 | pub struct KeywordMarkers { 58 | pub os_name: Option, 59 | pub sys_platform: Option, 60 | pub platform_machine: Option, 61 | pub platform_python_implementation: Option, 62 | pub platform_release: Option, 63 | pub platform_system: Option, 64 | pub platform_version: Option, 65 | pub python_version: Option, 66 | pub python_full_version: Option, 67 | pub implementation_name: Option, 68 | pub implementation_version: Option, 69 | } 70 | 71 | #[derive(Deserialize)] 72 | pub struct PipenvLock { 73 | /// Not used, this avoids having the section in `category_groups` below. 74 | #[allow(dead_code)] 75 | #[serde(rename = "_meta")] 76 | meta: Option, 77 | #[serde(flatten)] 78 | pub category_groups: Option>>, 79 | } 80 | 81 | #[derive(Deserialize)] 82 | pub struct LockedPackage { 83 | pub version: String, 84 | } 85 | 86 | #[derive(Deserialize)] 87 | pub struct Placeholder {} 88 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | icon: lucide/play 3 | --- 4 | # Usage 5 | 6 | ## Basic usage 7 | 8 | ```bash 9 | # With uv 10 | uvx migrate-to-uv 11 | 12 | # With pipx 13 | pipx run migrate-to-uv 14 | ``` 15 | 16 | For more advanced usages, see the [configuration](configuration.md) options. 17 | 18 | ## Migration errors 19 | 20 | Although `migrate-to-uv` tries its best to match the current package manager definition when performing the migration, 21 | some package managers have features that have no equivalent in uv or 22 | in [PEP 621](https://packaging.python.org/en/latest/specifications/pyproject-toml/#pyproject-toml-spec) specification 23 | that is followed by uv. 24 | 25 | In case the current package manager definition uses features that cannot be translated to uv, `migrate-to-uv` will abort 26 | the migration, pointing at the errors, and suggesting what to do before attempting the migration again, e.g.: 27 | 28 | ```console 29 | $ uvx migrate-to-uv 30 | error: Could not automatically migrate the project to uv because of the following errors: 31 | error: - Found multiple files ("README.md", "README2.md") in "tool.poetry.readme". PEP 621 only supports setting one. Make sure to manually edit the section before migrating. 32 | ``` 33 | 34 | For less problematic issues, `migrate-to-uv` will still perform the migration, but warn about what needs attention at 35 | the end of it, e.g.: 36 | 37 | ```console 38 | $ uvx migrate-to-uv 39 | [...] 40 | Successfully migrated project from Poetry to uv! 41 | 42 | warning: The following warnings occurred during the migration: 43 | warning: - Could not find dependency "non-existing-dependency" listed in "extra-with-non-existing-dependencies" extra. 44 | ``` 45 | 46 | ## Authentication for private indexes 47 | 48 | By default, `migrate-to-uv` generates `uv.lock` with `uv lock` to lock dependencies. If you currently use a package 49 | manager with private indexes, credentials will need to be set for locking to work properly. This can be done by setting 50 | the [same environment variables as uv expects for private indexes](https://docs.astral.sh/uv/concepts/indexes/#providing-credentials-directly). 51 | 52 | Since the names of the indexes in uv should be the same as the ones in the current package manager before the migration, 53 | you should be able to adapt the environment variables based on what you previously used. 54 | 55 | For instance, if you currently use Poetry and have: 56 | 57 | ```toml 58 | [[tool.poetry.source]] 59 | name = "foo-bar" 60 | url = "https://private-index.example.com" 61 | priority = "supplementary" 62 | ``` 63 | 64 | Credentials would be set with the following environment variables: 65 | 66 | - `POETRY_HTTP_BASIC_FOO_BAR_USERNAME` 67 | - `POETRY_HTTP_BASIC_FOO_BAR_PASSWORD` 68 | 69 | For uv, this would translate to: 70 | 71 | - `UV_INDEX_FOO_BAR_USERNAME` 72 | - `UV_INDEX_FOO_BAR_PASSWORD` 73 | 74 | To forward those credentials to `migrate-to-uv`, you can either export them beforehand, or set the environment variables 75 | when invoking the command: 76 | 77 | ```bash 78 | # Either 79 | export UV_INDEX_FOO_BAR_USERNAME= 80 | export UV_INDEX_FOO_BAR_PASSWORD= 81 | migrate-to-uv 82 | 83 | # Or 84 | UV_INDEX_FOO_BAR_USERNAME= \ 85 | UV_INDEX_FOO_BAR_PASSWORD= \ 86 | migrate-to-uv 87 | ``` 88 | -------------------------------------------------------------------------------- /src/converters/pyproject_updater.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::hatch::Hatch; 2 | use crate::schema::pep_621::Project; 3 | use crate::schema::pyproject::{BuildSystem, DependencyGroupSpecification}; 4 | use crate::schema::uv::Uv; 5 | use indexmap::IndexMap; 6 | use toml_edit::{DocumentMut, table, value}; 7 | 8 | /// Updates a `pyproject.toml` document. 9 | pub struct PyprojectUpdater<'a> { 10 | pub pyproject: &'a mut DocumentMut, 11 | } 12 | 13 | impl PyprojectUpdater<'_> { 14 | /// Adds or replaces PEP 621 data. 15 | pub fn insert_pep_621(&mut self, project: &Project) { 16 | self.pyproject["project"] = value( 17 | serde::Serialize::serialize(&project, toml_edit::ser::ValueSerializer::new()).unwrap(), 18 | ); 19 | } 20 | 21 | /// Adds or replaces dependency groups data in TOML document. 22 | pub fn insert_dependency_groups( 23 | &mut self, 24 | dependency_groups: Option<&IndexMap>>, 25 | ) { 26 | if let Some(dependency_groups) = dependency_groups { 27 | self.pyproject["dependency-groups"] = value( 28 | serde::Serialize::serialize( 29 | &dependency_groups, 30 | toml_edit::ser::ValueSerializer::new(), 31 | ) 32 | .unwrap(), 33 | ); 34 | } 35 | } 36 | 37 | /// Adds or replaces build system data. 38 | pub fn insert_build_system(&mut self, build_system: Option<&BuildSystem>) { 39 | if let Some(build_system) = build_system { 40 | self.pyproject["build-system"] = value( 41 | serde::Serialize::serialize(&build_system, toml_edit::ser::ValueSerializer::new()) 42 | .unwrap(), 43 | ); 44 | } 45 | } 46 | 47 | /// Adds or replaces uv-specific data in TOML document. 48 | pub fn insert_uv(&mut self, uv: &Uv) { 49 | if uv == &Uv::default() { 50 | return; 51 | } 52 | 53 | if !self.pyproject.contains_key("tool") { 54 | self.pyproject["tool"] = table(); 55 | } 56 | 57 | self.pyproject["tool"]["uv"] = value( 58 | serde::Serialize::serialize(&uv, toml_edit::ser::ValueSerializer::new()).unwrap(), 59 | ); 60 | } 61 | 62 | /// Adds or replaces hatch-specific data in TOML document. 63 | pub fn insert_hatch(&mut self, hatch: Option<&Hatch>) { 64 | if hatch.is_none() { 65 | return; 66 | } 67 | 68 | if !self.pyproject.contains_key("tool") { 69 | self.pyproject["tool"] = table(); 70 | } 71 | 72 | self.pyproject["tool"]["hatch"] = value( 73 | serde::Serialize::serialize(&hatch, toml_edit::ser::ValueSerializer::new()).unwrap(), 74 | ); 75 | } 76 | 77 | /// Remove `constraint-dependencies` under `[tool.uv]`, which is only needed to lock 78 | /// dependencies to specific versions in the generated lock file. 79 | pub fn remove_constraint_dependencies(&mut self) -> Option<&DocumentMut> { 80 | self.pyproject 81 | .get_mut("tool")? 82 | .as_table_mut()? 83 | .get_mut("uv")? 84 | .as_table_mut()? 85 | .remove("constraint-dependencies")?; 86 | 87 | // If `constraint-dependencies` was the only item in `[tool.uv]`, remove `[tool.uv]`. 88 | if self 89 | .pyproject 90 | .get("tool")? 91 | .as_table()? 92 | .get("uv")? 93 | .as_table()? 94 | .is_empty() 95 | { 96 | self.pyproject 97 | .get_mut("tool")? 98 | .as_table_mut()? 99 | .remove("uv")?; 100 | } 101 | 102 | Some(self.pyproject) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/converters/poetry/project.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::add_unrecoverable_error; 2 | use crate::schema::pep_621::AuthorOrMaintainer; 3 | use crate::schema::poetry::Script; 4 | use crate::schema::utils::SingleOrVec; 5 | use indexmap::IndexMap; 6 | use owo_colors::OwoColorize; 7 | use regex::Regex; 8 | use std::sync::LazyLock; 9 | 10 | static AUTHOR_REGEX: LazyLock = 11 | LazyLock::new(|| Regex::new(r"^(?[^<>]+)(?: <(?.+?)>)?$").unwrap()); 12 | 13 | pub fn get_readme(poetry_readme: Option>) -> Option { 14 | match poetry_readme { 15 | Some(SingleOrVec::Single(readme)) => Some(readme), 16 | Some(SingleOrVec::Vec(readmes)) => match readmes.as_slice() { 17 | [] => None, 18 | [readme] => Some(readme.clone()), 19 | _ => { 20 | add_unrecoverable_error(format!( 21 | "Found multiple files ({}) in \"{}\". PEP 621 only supports setting one. Make sure to manually edit the section before migrating.", 22 | readmes 23 | .iter() 24 | .map(|r| format!("\"{}\"", r.bold())) 25 | .collect::>() 26 | .join(", "), 27 | "tool.poetry.readme".bold(), 28 | )); 29 | None 30 | } 31 | }, 32 | None => None, 33 | } 34 | } 35 | 36 | pub fn get_authors(authors: Option>) -> Option> { 37 | Some( 38 | authors? 39 | .iter() 40 | .map(|p| { 41 | let captures = AUTHOR_REGEX.captures(p).unwrap(); 42 | 43 | AuthorOrMaintainer { 44 | name: captures.name("name").map(|m| m.as_str().into()), 45 | email: captures.name("email").map(|m| m.as_str().into()), 46 | } 47 | }) 48 | .collect(), 49 | ) 50 | } 51 | 52 | pub fn get_urls( 53 | poetry_urls: Option>, 54 | homepage: Option, 55 | repository: Option, 56 | documentation: Option, 57 | ) -> Option> { 58 | let mut urls: IndexMap = IndexMap::new(); 59 | 60 | if let Some(homepage) = homepage { 61 | urls.insert("Homepage".to_string(), homepage); 62 | } 63 | 64 | if let Some(repository) = repository { 65 | urls.insert("Repository".to_string(), repository); 66 | } 67 | 68 | if let Some(documentation) = documentation { 69 | urls.insert("Documentation".to_string(), documentation); 70 | } 71 | 72 | // URLs defined under `[tool.poetry.urls]` override whatever is set in `repository` or 73 | // `documentation` if there is a case-sensitive match. This is not the case for `homepage`, but 74 | // this is probably not an edge case worth handling. 75 | if let Some(poetry_urls) = poetry_urls { 76 | urls.extend(poetry_urls); 77 | } 78 | 79 | if urls.is_empty() { 80 | return None; 81 | } 82 | 83 | Some(urls) 84 | } 85 | 86 | pub fn get_scripts( 87 | poetry_scripts: Option>, 88 | scripts_from_plugins: Option>, 89 | ) -> Option> { 90 | let mut scripts: IndexMap = IndexMap::new(); 91 | 92 | if let Some(poetry_scripts) = poetry_scripts { 93 | for (name, script) in poetry_scripts { 94 | match script { 95 | Script::String(script) => { 96 | scripts.insert(name, script); 97 | } 98 | Script::Map { callable } => { 99 | if let Some(callable) = callable { 100 | scripts.insert(name, callable); 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | if let Some(scripts_from_plugins) = scripts_from_plugins { 108 | scripts.extend(scripts_from_plugins); 109 | } 110 | 111 | if scripts.is_empty() { 112 | return None; 113 | } 114 | Some(scripts) 115 | } 116 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use crate::converters::{BuildBackend, ConverterOptions, DependencyGroupsStrategy}; 2 | use crate::detector::{PackageManager, get_converter}; 3 | use crate::logger; 4 | use clap::Parser; 5 | use clap::builder::Styles; 6 | use clap::builder::styling::{AnsiColor, Effects}; 7 | use clap_verbosity_flag::{InfoLevel, Verbosity}; 8 | use log::error; 9 | use std::path::PathBuf; 10 | use std::process; 11 | 12 | const STYLES: Styles = Styles::styled() 13 | .header(AnsiColor::Green.on_default().effects(Effects::BOLD)) 14 | .usage(AnsiColor::Green.on_default().effects(Effects::BOLD)) 15 | .literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD)) 16 | .placeholder(AnsiColor::Cyan.on_default()) 17 | .error(AnsiColor::Red.on_default().effects(Effects::BOLD)) 18 | .valid(AnsiColor::Cyan.on_default().effects(Effects::BOLD)) 19 | .invalid(AnsiColor::Yellow.on_default().effects(Effects::BOLD)); 20 | 21 | #[derive(Parser)] 22 | #[command(version)] 23 | #[command(about = "Migrate a project to uv from another package manager.", long_about = None)] 24 | #[command(styles = STYLES)] 25 | #[allow(clippy::struct_excessive_bools)] 26 | struct Cli { 27 | #[arg(default_value = ".", help = "Path to the project to migrate")] 28 | path: PathBuf, 29 | #[arg( 30 | long, 31 | help = "Shows what changes would be applied, without modifying files" 32 | )] 33 | dry_run: bool, 34 | #[arg( 35 | long, 36 | help = "Do not lock dependencies with uv at the end of the migration" 37 | )] 38 | skip_lock: bool, 39 | #[arg( 40 | long, 41 | help = "Skip checks for whether or not the project is already using uv" 42 | )] 43 | skip_uv_checks: bool, 44 | #[arg( 45 | long, 46 | help = "Ignore current locked versions of dependencies when generating `uv.lock`" 47 | )] 48 | ignore_locked_versions: bool, 49 | #[arg( 50 | long, 51 | help = "Replace existing data in `[project]` section of `pyproject.toml` instead of keeping existing fields" 52 | )] 53 | replace_project_section: bool, 54 | #[arg( 55 | long, 56 | help = "Enforce a specific package manager instead of auto-detecting it" 57 | )] 58 | package_manager: Option, 59 | #[arg( 60 | long, 61 | default_value = "set-default-groups", 62 | help = "Strategy to use when migrating dependency groups" 63 | )] 64 | dependency_groups_strategy: DependencyGroupsStrategy, 65 | #[arg(long, help = "Enforce a specific build backend to use when migrating")] 66 | build_backend: Option, 67 | #[arg(long, help = "Keep data from current package manager")] 68 | keep_current_data: bool, 69 | #[arg(long, default_values = vec!["requirements.txt"], help = "Requirements file to migrate")] 70 | requirements_file: Vec, 71 | #[arg(long, default_values = vec!["requirements-dev.txt"], help = "Development requirements file to migrate")] 72 | dev_requirements_file: Vec, 73 | #[command(flatten)] 74 | verbose: Verbosity, 75 | } 76 | 77 | pub fn cli() { 78 | let cli = Cli::parse(); 79 | 80 | logger::configure(cli.verbose); 81 | 82 | let converter_options = ConverterOptions { 83 | project_path: PathBuf::from(&cli.path), 84 | dry_run: cli.dry_run, 85 | skip_lock: cli.skip_lock, 86 | skip_uv_checks: cli.skip_uv_checks, 87 | ignore_locked_versions: cli.ignore_locked_versions, 88 | replace_project_section: cli.replace_project_section, 89 | keep_old_metadata: cli.keep_current_data, 90 | dependency_groups_strategy: cli.dependency_groups_strategy, 91 | build_backend: cli.build_backend, 92 | }; 93 | 94 | match get_converter( 95 | &converter_options, 96 | cli.requirements_file, 97 | cli.dev_requirements_file, 98 | cli.package_manager, 99 | ) { 100 | Ok(converter) => { 101 | converter.convert_to_uv(); 102 | } 103 | Err(error) => { 104 | error!("{error}"); 105 | process::exit(1); 106 | } 107 | } 108 | 109 | process::exit(0) 110 | } 111 | -------------------------------------------------------------------------------- /src/toml.rs: -------------------------------------------------------------------------------- 1 | use toml_edit::visit_mut::{ 2 | VisitMut, visit_array_mut, visit_item_mut, visit_table_like_kv_mut, visit_table_mut, 3 | }; 4 | use toml_edit::{Array, InlineTable, Item, KeyMut, Value}; 5 | 6 | #[derive(Default)] 7 | pub struct PyprojectPrettyFormatter { 8 | pub parent_keys: Vec, 9 | } 10 | 11 | /// Prettifies Pyproject TOML based on usual conventions in the ecosystem. 12 | impl VisitMut for PyprojectPrettyFormatter { 13 | fn visit_item_mut(&mut self, node: &mut Item) { 14 | let parent_keys: Vec<&str> = self.parent_keys.iter().map(AsRef::as_ref).collect(); 15 | 16 | // Uv indexes are usually represented as array of tables (https://docs.astral.sh/uv/configuration/indexes/). 17 | if let ["tool", "uv", "index"] = parent_keys.as_slice() { 18 | let new_node = std::mem::take(node); 19 | let new_node = new_node 20 | .into_array_of_tables() 21 | .map_or_else(|i| i, Item::ArrayOfTables); 22 | 23 | *node = new_node; 24 | } 25 | 26 | visit_item_mut(self, node); 27 | } 28 | 29 | fn visit_table_mut(&mut self, node: &mut toml_edit::Table) { 30 | if !node.is_empty() { 31 | node.set_implicit(true); 32 | } 33 | 34 | visit_table_mut(self, node); 35 | } 36 | 37 | fn visit_table_like_kv_mut(&mut self, mut key: KeyMut<'_>, node: &mut Item) { 38 | self.parent_keys.push(key.to_string()); 39 | 40 | // Convert some inline tables into tables, when those tables are usually represented as 41 | // plain tables in the ecosystem. 42 | if let Item::Value(Value::InlineTable(inline_table)) = node { 43 | let parent_keys: Vec<&str> = self.parent_keys.iter().map(AsRef::as_ref).collect(); 44 | 45 | if matches!( 46 | parent_keys.as_slice(), 47 | ["build-system" | "project" | "dependency-groups"] 48 | | [ 49 | "project", 50 | "urls" 51 | | "optional-dependencies" 52 | | "scripts" 53 | | "gui-scripts" 54 | | "entry-points" 55 | ] 56 | | ["project", "entry-points", _] 57 | | ["tool", "uv"] 58 | | ["tool", "uv", "build-backend" | "sources"] 59 | | ["tool", "hatch", ..] 60 | ) { 61 | let position = match parent_keys.as_slice() { 62 | ["project"] => Some(0), 63 | ["dependency-groups"] => Some(1), 64 | ["tool", "uv"] => Some(2), 65 | ["tool", "hatch"] => Some(3), 66 | _ => None, 67 | }; 68 | 69 | let inline_table = std::mem::replace(inline_table, InlineTable::new()); 70 | let mut table = inline_table.into_table(); 71 | 72 | if let Some(position) = position { 73 | table.set_position(position); 74 | } 75 | 76 | key.fmt(); 77 | *node = Item::Table(table); 78 | } 79 | } 80 | 81 | // Ensure that a newline is inserted between the `[project]` section and the second section. 82 | // If we already had a prefix for the section, preserve it and prepend a newline. 83 | if let Some(table) = node.as_table_mut() 84 | && table.position() == Some(1) 85 | { 86 | if let Some(prefix) = table.clone().decor().prefix() { 87 | table 88 | .decor_mut() 89 | .set_prefix(format!("\n{}", prefix.as_str().unwrap_or_default())); 90 | } else { 91 | table.decor_mut().set_prefix("\n"); 92 | } 93 | } 94 | 95 | visit_table_like_kv_mut(self, key, node); 96 | 97 | self.parent_keys.pop(); 98 | } 99 | 100 | fn visit_array_mut(&mut self, node: &mut Array) { 101 | visit_array_mut(self, node); 102 | 103 | let parent_keys: Vec<&str> = self.parent_keys.iter().map(AsRef::as_ref).collect(); 104 | 105 | // It is common to have each array item on its own line if the array contains more than 2 106 | // items, so this applies this format on sections that were added. Targeting specific 107 | // sections ensures that unrelated sections are left intact. 108 | if matches!( 109 | parent_keys.as_slice(), 110 | ["project" | "dependency-groups", ..] | ["tool", "uv" | "hatch", ..] 111 | ) && node.len() >= 2 112 | { 113 | for item in node.iter_mut() { 114 | item.decor_mut().set_prefix("\n "); 115 | } 116 | 117 | node.set_trailing_comma(true); 118 | node.set_trailing("\n"); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/converters/pip/mod.rs: -------------------------------------------------------------------------------- 1 | mod dependencies; 2 | 3 | use crate::converters::Converter; 4 | use crate::converters::ConverterOptions; 5 | use crate::converters::pyproject_updater::PyprojectUpdater; 6 | use crate::schema::pep_621::Project; 7 | use crate::schema::pyproject::{DependencyGroupSpecification, PyProject}; 8 | use crate::schema::uv::Uv; 9 | use crate::toml::PyprojectPrettyFormatter; 10 | use indexmap::IndexMap; 11 | use std::default::Default; 12 | use std::fs; 13 | use toml_edit::DocumentMut; 14 | use toml_edit::visit_mut::VisitMut; 15 | 16 | #[derive(Debug, PartialEq, Eq)] 17 | pub struct Pip { 18 | pub converter_options: ConverterOptions, 19 | pub requirements_files: Vec, 20 | pub dev_requirements_files: Vec, 21 | pub is_pip_tools: bool, 22 | } 23 | 24 | impl Converter for Pip { 25 | fn build_uv_pyproject(&self) -> String { 26 | let pyproject_toml_content = 27 | fs::read_to_string(self.get_project_path().join("pyproject.toml")).unwrap_or_default(); 28 | let pyproject: PyProject = toml::from_str(pyproject_toml_content.as_str()).unwrap(); 29 | 30 | let dev_dependencies = dependencies::get( 31 | &self.get_project_path(), 32 | self.dev_requirements_files.clone(), 33 | ); 34 | 35 | let dependency_groups = dev_dependencies.map(|dependencies| { 36 | IndexMap::from([( 37 | "dev".to_string(), 38 | dependencies 39 | .iter() 40 | .map(|dep| DependencyGroupSpecification::String(dep.clone())) 41 | .collect(), 42 | )]) 43 | }); 44 | 45 | let project = Project { 46 | // "name" is required by uv. 47 | name: Some(String::new()), 48 | // "version" is required by uv. 49 | version: Some("0.0.1".to_string()), 50 | dependencies: dependencies::get( 51 | &self.get_project_path(), 52 | self.requirements_files.clone(), 53 | ), 54 | ..Default::default() 55 | }; 56 | 57 | let uv = Uv { 58 | package: Some(false), 59 | constraint_dependencies: self.get_constraint_dependencies(), 60 | ..Default::default() 61 | }; 62 | 63 | let pyproject_toml_content = 64 | fs::read_to_string(self.get_project_path().join("pyproject.toml")).unwrap_or_default(); 65 | let mut updated_pyproject = pyproject_toml_content.parse::().unwrap(); 66 | let mut pyproject_updater = PyprojectUpdater { 67 | pyproject: &mut updated_pyproject, 68 | }; 69 | 70 | pyproject_updater.insert_pep_621(&self.build_project(pyproject.project, project)); 71 | pyproject_updater.insert_dependency_groups(dependency_groups.as_ref()); 72 | pyproject_updater.insert_uv(&uv); 73 | 74 | let mut visitor = PyprojectPrettyFormatter::default(); 75 | visitor.visit_document_mut(&mut updated_pyproject); 76 | 77 | updated_pyproject.to_string() 78 | } 79 | 80 | fn get_package_manager_name(&self) -> String { 81 | if self.is_pip_tools { 82 | return "pip-tools".to_string(); 83 | } 84 | "pip".to_string() 85 | } 86 | 87 | fn get_converter_options(&self) -> &ConverterOptions { 88 | &self.converter_options 89 | } 90 | 91 | fn respect_locked_versions(&self) -> bool { 92 | // There are no locked dependencies for pip, so locked versions are only respected for 93 | // pip-tools. 94 | self.is_pip_tools && !self.get_converter_options().ignore_locked_versions 95 | } 96 | 97 | fn get_migrated_files_to_delete(&self) -> Vec { 98 | let mut files_to_delete: Vec = Vec::new(); 99 | 100 | for requirements_file in self 101 | .requirements_files 102 | .iter() 103 | .chain(&self.dev_requirements_files) 104 | { 105 | files_to_delete.push(requirements_file.clone()); 106 | 107 | // For pip-tools, also delete `.txt` files generated from `.in` files. 108 | if self.is_pip_tools { 109 | files_to_delete.push(requirements_file.replace(".in", ".txt")); 110 | } 111 | } 112 | 113 | files_to_delete 114 | } 115 | 116 | fn get_constraint_dependencies(&self) -> Option> { 117 | if !self.is_pip_tools || self.is_dry_run() || !self.respect_locked_versions() { 118 | return None; 119 | } 120 | 121 | if let Some(dependencies) = dependencies::get( 122 | self.get_project_path().as_path(), 123 | self.requirements_files 124 | .clone() 125 | .into_iter() 126 | .chain(self.dev_requirements_files.clone()) 127 | .map(|f| f.replace(".in", ".txt")) 128 | .collect(), 129 | ) { 130 | if dependencies.is_empty() { 131 | return None; 132 | } 133 | return Some(dependencies); 134 | } 135 | None 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | env: 9 | PYTHON_VERSION: '3.14' 10 | # renovate: datasource=pypi depName=uv 11 | UV_VERSION: '0.9.18' 12 | # renovate: datasource=pypi depName=pypi-attestations 13 | PYPI_ATTESTATIONS_VERSION: '0.0.29' 14 | 15 | permissions: {} 16 | 17 | jobs: 18 | linux: 19 | runs-on: ubuntu-24.04 20 | strategy: 21 | matrix: 22 | target: [x86_64, aarch64] 23 | manylinux: [auto, musllinux_1_1] 24 | steps: 25 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 26 | with: 27 | persist-credentials: false 28 | 29 | - name: Build wheels 30 | uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1 31 | with: 32 | target: ${{ matrix.target }} 33 | manylinux: ${{ matrix.manylinux }} 34 | args: --release --out dist 35 | 36 | - name: Upload wheels 37 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 38 | with: 39 | name: wheels-linux-${{ matrix.target }}-${{ matrix.manylinux }} 40 | path: dist 41 | 42 | windows: 43 | runs-on: windows-2025 44 | strategy: 45 | matrix: 46 | target: [x64, aarch64] 47 | steps: 48 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 49 | with: 50 | persist-credentials: false 51 | 52 | - name: Build wheels 53 | uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1 54 | with: 55 | target: ${{ matrix.target }} 56 | args: --release --out dist 57 | 58 | - name: Upload wheels 59 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 60 | with: 61 | name: wheels-windows-${{ matrix.target }} 62 | path: dist 63 | 64 | macos: 65 | runs-on: macos-26 66 | strategy: 67 | matrix: 68 | target: [x86_64, aarch64] 69 | steps: 70 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 71 | with: 72 | persist-credentials: false 73 | 74 | - name: Build wheels 75 | uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1 76 | with: 77 | target: ${{ matrix.target }} 78 | args: --release --out dist 79 | 80 | - name: Upload wheels 81 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 82 | with: 83 | name: wheels-macos-${{ matrix.target }} 84 | path: dist 85 | 86 | sdist: 87 | runs-on: ubuntu-24.04 88 | steps: 89 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 90 | with: 91 | persist-credentials: false 92 | 93 | - name: Build sdist 94 | uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1 95 | with: 96 | command: sdist 97 | args: --out dist 98 | 99 | - name: Upload sdist 100 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 101 | with: 102 | name: wheels-sdist 103 | path: dist 104 | 105 | publish: 106 | name: Publish 107 | runs-on: ubuntu-24.04 108 | needs: [linux, windows, macos, sdist] 109 | environment: pypi 110 | permissions: 111 | id-token: write 112 | steps: 113 | - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 114 | 115 | - name: Install uv 116 | uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7 117 | with: 118 | version: ${{ env.UV_VERSION }} 119 | 120 | - name: Generate PyPI attestations 121 | run: uvx pypi-attestations@${{ env.PYPI_ATTESTATIONS_VERSION }} sign wheels-*/* 122 | 123 | - name: Publish to PyPI 124 | if: ${{ github.event_name == 'release' }} 125 | run: uv publish --trusted-publishing always wheels-*/* 126 | 127 | - name: '[dry-run] Publish to PyPI' 128 | if: ${{ github.event_name != 'release' }} 129 | run: uv publish --trusted-publishing always --dry-run wheels-*/* 130 | 131 | build-docs: 132 | name: Build documentation 133 | runs-on: ubuntu-24.04 134 | needs: publish 135 | permissions: 136 | contents: write 137 | steps: 138 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 139 | with: 140 | persist-credentials: false 141 | 142 | - name: Install uv 143 | uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7 144 | with: 145 | version: ${{ env.UV_VERSION }} 146 | 147 | - name: Install Python 148 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6 149 | with: 150 | python-version: ${{ env.PYTHON_VERSION }} 151 | 152 | - name: Build documentation 153 | run: uv run --only-group docs zensical build --clean 154 | 155 | - name: Upload documentation as artifact 156 | id: deployment 157 | uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4 158 | with: 159 | path: site 160 | 161 | publish-docs: 162 | name: Publish documentation 163 | runs-on: ubuntu-24.04 164 | needs: build-docs 165 | permissions: 166 | pages: write 167 | id-token: write 168 | environment: 169 | name: github-pages 170 | url: ${{ steps.deployment.outputs.page_url }} 171 | if: ${{ github.event_name == 'release' }} 172 | steps: 173 | - name: Deploy to GitHub Pages 174 | id: deployment 175 | uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 176 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | icon: lucide/wrench 3 | --- 4 | # Configuration 5 | 6 | ## Project path 7 | 8 | By default, `migrate-to-uv` uses the current directory to search for the project to migrate. If the project is in a 9 | different path, you can set the path to a directory as a positional argument. 10 | 11 | **Example**: 12 | 13 | ```bash 14 | # Relative path 15 | migrate-to-uv subdirectory 16 | 17 | # Absolute path 18 | migrate-to-uv /home/foo/project 19 | ``` 20 | 21 | ## Arguments 22 | 23 | `migrate-to-uv` provides a few arguments to let you customize how the migration is performed. 24 | 25 | ### `--dry-run` 26 | 27 | Run the migration without modifying the files, printing the changes that would have been made in the terminal instead. 28 | 29 | **Example**: 30 | 31 | ```bash 32 | migrate-to-uv --dry-run 33 | ``` 34 | 35 | ### `--skip-lock` 36 | 37 | By default, `migrate-to-uv` locks dependencies with `uv lock` at the end of the migration. This flag disables this 38 | behavior. 39 | 40 | **Example**: 41 | 42 | ```bash 43 | migrate-to-uv --skip-lock 44 | ``` 45 | 46 | ### `--skip-uv-checks` 47 | 48 | By default, `migrate-to-uv` will exit early if a project already uses uv. This flag disables this behavior, allowing 49 | `migrate-to-uv` to run on a `pyproject.toml` that already has uv configured. 50 | 51 | Note that the project must also have a valid non-uv package manager configured, otherwise it will fail to generate the 52 | uv configuration. 53 | 54 | **Example**: 55 | 56 | ```bash 57 | migrate-to-uv --skip-uv-checks 58 | ``` 59 | 60 | ### `--ignore-locked-versions` 61 | 62 | By default, when locking dependencies with `uv lock`, `migrate-to-uv` keeps dependencies to the versions they were 63 | locked to with the previous package manager, if it supports lock files, and if a lock file is found. This behavior can 64 | be disabled, in which case dependencies will be locked to the highest possible versions allowed by the dependencies 65 | constraints. 66 | 67 | **Example**: 68 | 69 | ```bash 70 | migrate-to-uv --ignore-locked-versions 71 | ``` 72 | 73 | ### `--replace-project-section` 74 | 75 | By default, existing data in `[project]` section of `pyproject.toml` is preserved when migrating. This flag allows 76 | completely replacing existing content. 77 | 78 | **Example**: 79 | 80 | ```bash 81 | migrate-to-uv --replace-project-section 82 | ``` 83 | 84 | ### `--package-manager` 85 | 86 | By default, `migrate-to-uv` tries to auto-detect the package manager based on the files (and their content) used by the 87 | package managers it supports. If auto-detection does not work in some cases, or if you prefer to explicitly specify the 88 | package manager, you can explicitly set it. 89 | 90 | **Available options**: 91 | 92 | - `pip` 93 | - `pip-tools` 94 | - `pipenv` 95 | - `poetry` 96 | 97 | **Example**: 98 | 99 | ```bash 100 | migrate-to-uv --package-manager poetry 101 | ``` 102 | 103 | ### `--build-backend` 104 | 105 | The build backend to choose when performing the migration. If the option is not provided, `hatch` is chosen by default. 106 | 107 | !!!info 108 | 109 | Support for `uv` is considered experimental, but the default will likely change in the future, where the build 110 | backend will be chosen based on the complexity of the package distribution metadata. 111 | 112 | !!!note 113 | 114 | If you choose `uv` and the migration cannot be performed because the project uses package distribution metadata that 115 | cannot be expressed with uv build backend, the migration will fail, suggesting to use hatch with 116 | `--build-backend hatch`. 117 | 118 | **Available options**: 119 | 120 | - `hatch` 121 | - `uv` 122 | 123 | **Example**: 124 | 125 | ```bash 126 | migrate-to-uv --build-backend uv 127 | ``` 128 | 129 | ### `--dependency-groups-strategy` 130 | 131 | Most package managers that support dependency groups install dependencies from all groups when performing installation. 132 | By default, uv will [only install `dev` one](https://docs.astral.sh/uv/concepts/projects/dependencies/#default-groups). 133 | 134 | In order to match the current package manager as closely as possible, `migrate-to-uv` sets all dependency groups (except 135 | the ones that could be optional, 136 | like [Poetry allows to do](https://python-poetry.org/docs/managing-dependencies#optional-groups)) in `default-groups` 137 | under `[tool.uv]` section. If the only dependency group is `dev`, `default-groups` is not set, as uv already defaults to 138 | including only `dev` group. 139 | 140 | If the default behavior is not suitable, it is possible to change it. 141 | 142 | **Available options**: 143 | 144 | - `set-default-groups` (default): Move each dependency group to its corresponding uv dependency group, and add all 145 | non-optional dependency groups in `default-groups` under `[tool.uv]` section (unless the only dependency group is 146 | `dev` one, as this is already uv's default) 147 | - `include-in-dev`: Move each dependency group to its corresponding uv dependency group, and reference all non-optional 148 | dependency groups (others than `dev` one) in `dev` dependency group by using `{ include-group = "" }` 149 | - `keep-existing`: Move each dependency group to its corresponding uv dependency group, without any further action 150 | - `merge-into-dev`: Merge dependencies from all non-optional dependency groups into `dev` dependency group (optional 151 | dependency groups are moved to their corresponding uv dependency groups) 152 | 153 | **Example**: 154 | 155 | ```bash 156 | migrate-to-uv --dependency-groups-strategy include-in-dev 157 | ``` 158 | 159 | ### `--requirements-file` 160 | 161 | Names of the production requirements files to look for, for projects using `pip` or `pip-tools`. The argument can be set 162 | multiple times, if there are multiple files. 163 | 164 | **Example**: 165 | 166 | ```bash 167 | migrate-to-uv --requirements-file requirements.txt --requirements-file more-requirements.txt 168 | ``` 169 | 170 | ### `--dev-requirements-file` 171 | 172 | Names of the development requirements files to look for, for projects using `pip` or `pip-tools`. The argument can be 173 | set multiple times, if there are multiple files. 174 | 175 | **Example**: 176 | 177 | ```bash 178 | migrate-to-uv --dev-requirements-file requirements-dev.txt --dev-requirements-file requirements-docs.txt 179 | ``` 180 | 181 | ### `--keep-current-data` 182 | 183 | Keep the current package manager data (lock file, sections in `pyproject.toml`, ...) after the migration, if you want to 184 | handle the cleaning yourself, or want to compare the differences first. 185 | 186 | **Example**: 187 | 188 | ```bash 189 | migrate-to-uv --keep-current-data 190 | ``` 191 | -------------------------------------------------------------------------------- /src/converters/pipenv/mod.rs: -------------------------------------------------------------------------------- 1 | mod dependencies; 2 | mod project; 3 | mod sources; 4 | 5 | use crate::converters::Converter; 6 | use crate::converters::ConverterOptions; 7 | use crate::converters::pyproject_updater::PyprojectUpdater; 8 | use crate::errors::add_recoverable_error; 9 | use crate::schema::pep_621::Project; 10 | use crate::schema::pipenv::{PipenvLock, Pipfile}; 11 | use crate::schema::pyproject::PyProject; 12 | use crate::schema::uv::{SourceContainer, Uv}; 13 | use crate::toml::PyprojectPrettyFormatter; 14 | use indexmap::IndexMap; 15 | use owo_colors::OwoColorize; 16 | use std::default::Default; 17 | use std::fs; 18 | use toml_edit::DocumentMut; 19 | use toml_edit::visit_mut::VisitMut; 20 | 21 | #[derive(Debug, PartialEq, Eq)] 22 | pub struct Pipenv { 23 | pub converter_options: ConverterOptions, 24 | } 25 | 26 | impl Converter for Pipenv { 27 | fn build_uv_pyproject(&self) -> String { 28 | let pyproject_toml_content = 29 | fs::read_to_string(self.get_project_path().join("pyproject.toml")).unwrap_or_default(); 30 | let pyproject: PyProject = toml::from_str(pyproject_toml_content.as_str()).unwrap(); 31 | 32 | let pipfile_content = fs::read_to_string(self.get_project_path().join("Pipfile")).unwrap(); 33 | let pipfile: Pipfile = toml::from_str(pipfile_content.as_str()).unwrap(); 34 | 35 | let mut uv_source_index: IndexMap = IndexMap::new(); 36 | let (dependency_groups, uv_default_groups) = 37 | dependencies::get_dependency_groups_and_default_groups( 38 | &pipfile, 39 | &mut uv_source_index, 40 | self.get_dependency_groups_strategy(), 41 | ); 42 | 43 | let project = Project { 44 | // "name" is required by uv. 45 | name: Some(String::new()), 46 | // "version" is required by uv. 47 | version: Some("0.0.1".to_string()), 48 | requires_python: project::get_requires_python(pipfile.requires), 49 | dependencies: dependencies::get(pipfile.packages.as_ref(), &mut uv_source_index), 50 | ..Default::default() 51 | }; 52 | 53 | let uv = Uv { 54 | package: Some(false), 55 | index: sources::get_indexes(pipfile.source), 56 | sources: if uv_source_index.is_empty() { 57 | None 58 | } else { 59 | Some(uv_source_index) 60 | }, 61 | default_groups: uv_default_groups, 62 | constraint_dependencies: self.get_constraint_dependencies(), 63 | ..Uv::default() 64 | }; 65 | 66 | let pyproject_toml_content = 67 | fs::read_to_string(self.get_project_path().join("pyproject.toml")).unwrap_or_default(); 68 | let mut updated_pyproject = pyproject_toml_content.parse::().unwrap(); 69 | let mut pyproject_updater = PyprojectUpdater { 70 | pyproject: &mut updated_pyproject, 71 | }; 72 | 73 | pyproject_updater.insert_pep_621(&self.build_project(pyproject.project, project)); 74 | pyproject_updater.insert_dependency_groups(dependency_groups.as_ref()); 75 | pyproject_updater.insert_uv(&uv); 76 | 77 | let mut visitor = PyprojectPrettyFormatter::default(); 78 | visitor.visit_document_mut(&mut updated_pyproject); 79 | 80 | updated_pyproject.to_string() 81 | } 82 | 83 | fn get_package_manager_name(&self) -> String { 84 | "Pipenv".to_string() 85 | } 86 | 87 | fn get_converter_options(&self) -> &ConverterOptions { 88 | &self.converter_options 89 | } 90 | 91 | fn get_migrated_files_to_delete(&self) -> Vec { 92 | vec!["Pipfile".to_string(), "Pipfile.lock".to_string()] 93 | } 94 | 95 | fn get_constraint_dependencies(&self) -> Option> { 96 | let pipenv_lock_path = self.get_project_path().join("Pipfile.lock"); 97 | 98 | if self.is_dry_run() || !self.respect_locked_versions() || !pipenv_lock_path.exists() { 99 | return None; 100 | } 101 | 102 | let pipenv_lock_content = fs::read_to_string(pipenv_lock_path).unwrap(); 103 | let Ok(pipenv_lock) = serde_json::from_str::(pipenv_lock_content.as_str()) 104 | else { 105 | add_recoverable_error(format!( 106 | "\"{}\" could not be parsed, so dependencies were not kept to their previous locked versions.", 107 | "Pipfile.lock".bold(), 108 | )); 109 | return None; 110 | }; 111 | 112 | let constraint_dependencies: Vec = pipenv_lock 113 | .category_groups 114 | .unwrap_or_default() 115 | .values() 116 | .flatten() 117 | .map(|(name, spec)| format!("{}{}", name, spec.version)) 118 | .collect(); 119 | 120 | if constraint_dependencies.is_empty() { 121 | None 122 | } else { 123 | Some(constraint_dependencies) 124 | } 125 | } 126 | } 127 | 128 | #[cfg(test)] 129 | mod tests { 130 | use super::*; 131 | use crate::converters::DependencyGroupsStrategy; 132 | use std::fs::File; 133 | use std::io::Write; 134 | use std::path::PathBuf; 135 | use tempfile::tempdir; 136 | 137 | #[test] 138 | fn test_perform_migration_python_full_version() { 139 | let tmp_dir = tempdir().unwrap(); 140 | let project_path = tmp_dir.path(); 141 | 142 | let pipfile_content = r#" 143 | [requires] 144 | python_full_version = "3.13.1" 145 | "#; 146 | 147 | let mut pipfile_file = File::create(project_path.join("Pipfile")).unwrap(); 148 | pipfile_file.write_all(pipfile_content.as_bytes()).unwrap(); 149 | 150 | let pipenv = Pipenv { 151 | converter_options: ConverterOptions { 152 | project_path: PathBuf::from(project_path), 153 | dry_run: true, 154 | skip_lock: true, 155 | skip_uv_checks: false, 156 | ignore_locked_versions: true, 157 | replace_project_section: false, 158 | keep_old_metadata: false, 159 | dependency_groups_strategy: DependencyGroupsStrategy::SetDefaultGroups, 160 | build_backend: None, 161 | }, 162 | }; 163 | 164 | insta::assert_snapshot!(pipenv.build_uv_pyproject(), @r###" 165 | [project] 166 | name = "" 167 | version = "0.0.1" 168 | requires-python = "==3.13.1" 169 | 170 | [tool.uv] 171 | package = false 172 | "###); 173 | } 174 | 175 | #[test] 176 | fn test_perform_migration_empty_requires() { 177 | let tmp_dir = tempdir().unwrap(); 178 | let project_path = tmp_dir.path(); 179 | 180 | let pipfile_content = "[requires]"; 181 | 182 | let mut pipfile_file = File::create(project_path.join("Pipfile")).unwrap(); 183 | pipfile_file.write_all(pipfile_content.as_bytes()).unwrap(); 184 | 185 | let pipenv = Pipenv { 186 | converter_options: ConverterOptions { 187 | project_path: PathBuf::from(project_path), 188 | dry_run: true, 189 | skip_lock: true, 190 | skip_uv_checks: false, 191 | ignore_locked_versions: true, 192 | replace_project_section: false, 193 | keep_old_metadata: false, 194 | dependency_groups_strategy: DependencyGroupsStrategy::SetDefaultGroups, 195 | build_backend: None, 196 | }, 197 | }; 198 | 199 | insta::assert_snapshot!(pipenv.build_uv_pyproject(), @r###" 200 | [project] 201 | name = "" 202 | version = "0.0.1" 203 | 204 | [tool.uv] 205 | package = false 206 | "###); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/schema/poetry.rs: -------------------------------------------------------------------------------- 1 | use crate::converters::poetry::version::{ParseVersionError, ParseVersionErrorKind, PoetryPep440}; 2 | use crate::schema::utils::SingleOrVec; 3 | use indexmap::IndexMap; 4 | use serde::{Deserialize, Serialize}; 5 | use std::str::FromStr; 6 | 7 | #[derive(Deserialize, Serialize, Default)] 8 | pub struct Poetry { 9 | #[serde(rename = "package-mode")] 10 | pub package_mode: Option, 11 | pub name: Option, 12 | pub version: Option, 13 | pub description: Option, 14 | pub authors: Option>, 15 | pub license: Option, 16 | pub maintainers: Option>, 17 | pub readme: Option>, 18 | pub homepage: Option, 19 | pub repository: Option, 20 | pub documentation: Option, 21 | pub keywords: Option>, 22 | pub classifiers: Option>, 23 | pub source: Option>, 24 | pub dependencies: Option>, 25 | pub extras: Option>>, 26 | #[serde(rename = "dev-dependencies")] 27 | pub dev_dependencies: Option>, 28 | pub group: Option>, 29 | pub urls: Option>, 30 | pub scripts: Option>, 31 | pub plugins: Option>>, 32 | pub packages: Option>, 33 | pub include: Option>, 34 | pub exclude: Option>, 35 | } 36 | 37 | #[derive(Deserialize, Serialize)] 38 | pub struct DependencyGroup { 39 | pub dependencies: IndexMap, 40 | pub optional: Option, 41 | } 42 | 43 | /// Represents a package source: . 44 | #[derive(Deserialize, Serialize)] 45 | pub struct Source { 46 | pub name: String, 47 | pub url: Option, 48 | pub priority: Option, 49 | } 50 | 51 | #[derive(Deserialize, Serialize, Eq, PartialEq, Debug)] 52 | #[serde(rename_all = "lowercase")] 53 | pub enum SourcePriority { 54 | /// . 55 | Primary, 56 | /// . 57 | Supplemental, 58 | /// . 59 | Explicit, 60 | /// . 61 | Default, 62 | /// . 63 | Secondary, 64 | } 65 | 66 | /// Represents the different ways a script can be defined in Poetry. 67 | #[derive(Deserialize, Serialize)] 68 | #[serde(untagged)] 69 | pub enum Script { 70 | String(String), 71 | // Although not documented, a script can be set as a map, where `callable` is the script to run. 72 | // An `extra` field also exists, but it doesn't seem to actually do 73 | // anything (https://github.com/python-poetry/poetry/issues/6892). 74 | Map { callable: Option }, 75 | } 76 | 77 | /// Represents the different ways dependencies can be defined in Poetry. 78 | /// 79 | /// See for details. 80 | #[derive(Deserialize, Serialize)] 81 | #[serde(untagged)] 82 | #[allow(clippy::large_enum_variant)] 83 | pub enum DependencySpecification { 84 | /// Simple version constraint: . 85 | String(String), 86 | /// Complex version constraint: . 87 | Map { 88 | version: Option, 89 | extras: Option>, 90 | markers: Option, 91 | python: Option, 92 | platform: Option, 93 | source: Option, 94 | git: Option, 95 | branch: Option, 96 | rev: Option, 97 | tag: Option, 98 | subdirectory: Option, 99 | path: Option, 100 | develop: Option, 101 | url: Option, 102 | }, 103 | /// Multiple constraints dependencies: . 104 | Vec(Vec), 105 | } 106 | 107 | impl DependencySpecification { 108 | pub fn to_pep_508(&self) -> Result { 109 | match self { 110 | Self::String(version) => Ok(PoetryPep440::from_str(version)?.to_string()), 111 | Self::Map { 112 | version, extras, .. 113 | } => { 114 | let mut pep_508_version = String::new(); 115 | 116 | if let Some(extras) = extras { 117 | pep_508_version.push_str(format!("[{}]", extras.join(", ")).as_str()); 118 | } 119 | 120 | if let Some(version) = version { 121 | pep_508_version.push_str(PoetryPep440::from_str(version)?.to_string().as_str()); 122 | } 123 | 124 | if let Some(marker) = self.get_marker()? { 125 | pep_508_version.push_str(format!(" ; {marker}").as_str()); 126 | } 127 | 128 | Ok(pep_508_version) 129 | } 130 | Self::Vec(_) => Ok(String::new()), 131 | } 132 | } 133 | 134 | pub fn get_marker(&self) -> Result, ParseVersionError> { 135 | let mut combined_markers: Vec = Vec::new(); 136 | 137 | if let Self::Map { 138 | python, 139 | markers, 140 | platform, 141 | .. 142 | } = self 143 | { 144 | if let Some(python) = python { 145 | match PoetryPep440::from_str(python) { 146 | Ok(version) => combined_markers.push(version.to_python_marker()), 147 | Err(ParseVersionError { 148 | kind: ParseVersionErrorKind::OrOperator(operator), 149 | version, 150 | }) => { 151 | return Err(ParseVersionError::new( 152 | ParseVersionErrorKind::PythonMarkerOrOperator(operator), 153 | version, 154 | )); 155 | } 156 | Err(ParseVersionError { 157 | kind: ParseVersionErrorKind::Other, 158 | version, 159 | }) => { 160 | return Err(ParseVersionError::new( 161 | ParseVersionErrorKind::PythonMarker, 162 | version, 163 | )); 164 | } 165 | Err(e) => return Err(e), 166 | } 167 | } 168 | 169 | if let Some(markers) = markers { 170 | combined_markers.push(markers.clone()); 171 | } 172 | 173 | if let Some(platform) = platform { 174 | combined_markers.push(format!("sys_platform == '{platform}'")); 175 | } 176 | } 177 | 178 | if combined_markers.is_empty() { 179 | return Ok(None); 180 | } 181 | Ok(Some(combined_markers.join(" and "))) 182 | } 183 | } 184 | 185 | /// Package distribution definition . 186 | #[derive(Deserialize, Serialize)] 187 | pub struct Package { 188 | pub include: String, 189 | pub from: Option, 190 | pub to: Option, 191 | pub format: Option>, 192 | } 193 | 194 | /// Package distribution file inclusion: . 195 | #[derive(Deserialize, Serialize)] 196 | #[serde(untagged)] 197 | pub enum Include { 198 | String(String), 199 | Map { 200 | path: String, 201 | format: Option>, 202 | }, 203 | } 204 | 205 | #[derive(Deserialize, Serialize, Eq, PartialEq)] 206 | #[serde(rename_all = "lowercase")] 207 | pub enum Format { 208 | Sdist, 209 | Wheel, 210 | } 211 | 212 | #[derive(Deserialize)] 213 | pub struct PoetryLock { 214 | pub package: Option>, 215 | } 216 | 217 | #[derive(Deserialize)] 218 | pub struct LockedPackage { 219 | pub name: String, 220 | pub version: String, 221 | } 222 | -------------------------------------------------------------------------------- /tests/fixtures/pipenv/with_lock_file/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "7ecb5e9587be05fc0f32ad07c192fc2216c54cfb965db1d829c1659de7a19c5f" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "arrow": { 18 | "hashes": [ 19 | "sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1", 20 | "sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2" 21 | ], 22 | "index": "pypi", 23 | "markers": "python_version >= '3.6'", 24 | "version": "==1.2.3" 25 | }, 26 | "faker": { 27 | "hashes": [ 28 | "sha256:1c925fc0e86a51fc46648b504078c88d0cd48da1da2595c4e712841cab43a1e4", 29 | "sha256:d30c5f0e2796b8970de68978365247657486eb0311c5abe88d0b895b68dff05d" 30 | ], 31 | "index": "pypi", 32 | "markers": "python_version >= '3.8'", 33 | "version": "==33.1.0" 34 | }, 35 | "python-dateutil": { 36 | "hashes": [ 37 | "sha256:07009062406cffd554a9b4135cd2ff167c9bf6b7aac61fe946c93e69fad1bbd8", 38 | "sha256:8f95bb7e6edbb2456a51a1fb58c8dca942024b4f5844cae62c90aa88afe6e300" 39 | ], 40 | "index": "pypi", 41 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", 42 | "version": "==2.7.0" 43 | }, 44 | "six": { 45 | "hashes": [ 46 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 47 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 48 | ], 49 | "index": "pypi", 50 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", 51 | "version": "==1.15.0" 52 | }, 53 | "typing-extensions": { 54 | "hashes": [ 55 | "sha256:6ad00b63f849b7dcc313b70b6b304ed67b2b2963b3098a33efe18056b1a9a223", 56 | "sha256:ff6b238610c747e44c268aa4bb23c8c735d665a63726df3f9431ce707f2aa768" 57 | ], 58 | "index": "pypi", 59 | "markers": "python_version >= '3.7'", 60 | "version": "==4.6.0" 61 | } 62 | }, 63 | "develop": { 64 | "mypy": { 65 | "hashes": [ 66 | "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", 67 | "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", 68 | "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f", 69 | "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74", 70 | "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a", 71 | "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", 72 | "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", 73 | "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", 74 | "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", 75 | "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", 76 | "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d", 77 | "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6", 78 | "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", 79 | "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", 80 | "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", 81 | "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", 82 | "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a", 83 | "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc", 84 | "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7", 85 | "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb", 86 | "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", 87 | "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732", 88 | "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80", 89 | "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", 90 | "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", 91 | "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", 92 | "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", 93 | "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24", 94 | "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", 95 | "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b", 96 | "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372", 97 | "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8" 98 | ], 99 | "index": "pypi", 100 | "markers": "python_version >= '3.8'", 101 | "version": "==1.13.0" 102 | }, 103 | "mypy-extensions": { 104 | "hashes": [ 105 | "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", 106 | "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" 107 | ], 108 | "markers": "python_version >= '3.5'", 109 | "version": "==1.0.0" 110 | }, 111 | "typing-extensions": { 112 | "hashes": [ 113 | "sha256:6ad00b63f849b7dcc313b70b6b304ed67b2b2963b3098a33efe18056b1a9a223", 114 | "sha256:ff6b238610c747e44c268aa4bb23c8c735d665a63726df3f9431ce707f2aa768" 115 | ], 116 | "index": "pypi", 117 | "markers": "python_version >= '3.7'", 118 | "version": "==4.6.0" 119 | } 120 | }, 121 | "test": { 122 | "factory-boy": { 123 | "hashes": [ 124 | "sha256:a98d277b0c047c75eb6e4ab8508a7f81fb03d2cb21986f627913546ef7a2a55e", 125 | "sha256:eb02a7dd1b577ef606b75a253b9818e6f9eaf996d94449c9d5ebb124f90dc795" 126 | ], 127 | "index": "pypi", 128 | "markers": "python_version >= '3.6'", 129 | "version": "==3.2.1" 130 | }, 131 | "faker": { 132 | "hashes": [ 133 | "sha256:1c925fc0e86a51fc46648b504078c88d0cd48da1da2595c4e712841cab43a1e4", 134 | "sha256:d30c5f0e2796b8970de68978365247657486eb0311c5abe88d0b895b68dff05d" 135 | ], 136 | "index": "pypi", 137 | "markers": "python_version >= '3.8'", 138 | "version": "==33.1.0" 139 | }, 140 | "python-dateutil": { 141 | "hashes": [ 142 | "sha256:07009062406cffd554a9b4135cd2ff167c9bf6b7aac61fe946c93e69fad1bbd8", 143 | "sha256:8f95bb7e6edbb2456a51a1fb58c8dca942024b4f5844cae62c90aa88afe6e300" 144 | ], 145 | "index": "pypi", 146 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", 147 | "version": "==2.7.0" 148 | }, 149 | "six": { 150 | "hashes": [ 151 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 152 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 153 | ], 154 | "index": "pypi", 155 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", 156 | "version": "==1.15.0" 157 | }, 158 | "typing-extensions": { 159 | "hashes": [ 160 | "sha256:6ad00b63f849b7dcc313b70b6b304ed67b2b2963b3098a33efe18056b1a9a223", 161 | "sha256:ff6b238610c747e44c268aa4bb23c8c735d665a63726df3f9431ce707f2aa768" 162 | ], 163 | "index": "pypi", 164 | "markers": "python_version >= '3.7'", 165 | "version": "==4.6.0" 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.9.1 - 2025-12-24 4 | 5 | ### Bug fixes 6 | 7 | * [poetry] Fail migration on missing `__init__.py` for uv build backend ([#553](https://github.com/mkniewallner/migrate-to-uv/pull/553)) 8 | 9 | ## 0.9.0 - 2025-12-22 10 | 11 | ### New features 12 | 13 | #### Experimental support for uv build backend 14 | 15 | When migrating Poetry package distribution metadata, `migrate-to-uv` uses [Hatch](https://hatch.pypa.io/latest/config/build/) build backend. Experimental support for migrating to uv build backend has been added behind `--build backend-uv` argument. 16 | 17 | Note that uv build backend offers less flexibility than Poetry and Hatch, so the migration might be aborted if some options used by Poetry cannot be expressed with uv build backend. If you try `--build-backend uv` and encounter any issue, feel free to report it. 18 | 19 | ### Features 20 | 21 | * [poetry] Add experimental `--build-backend uv` argument to migrate package distribution metadata to uv build backend ([#533](https://github.com/mkniewallner/migrate-to-uv/pull/533)) 22 | 23 | ### Bug fixes 24 | 25 | * [poetry] Do not migrate `packages`/`include` with empty `format` array ([#538](https://github.com/mkniewallner/migrate-to-uv/pull/538)) 26 | * [poetry] Fail migration on unhandled python marker specification ([#544](https://github.com/mkniewallner/migrate-to-uv/pull/544)) 27 | 28 | ## 0.8.1 - 2025-12-13 29 | 30 | ### Bug fixes 31 | 32 | * [poetry] Fail on unhandled version specifications ([#514](https://github.com/mkniewallner/migrate-to-uv/pull/514)) 33 | * [poetry] Also check if `poetry.lock` exists when checking if a project uses Poetry ([#528](https://github.com/mkniewallner/migrate-to-uv/pull/528)) 34 | 35 | ## 0.8.0 - 2025-11-17 36 | 37 | ### Breaking changes 38 | 39 | #### Abort on unrecoverable errors and warn about recoverable ones 40 | 41 | Although `migrate-to-uv` tries its best to migrate a project to uv without changing the behavior, some things that are accepted by package managers do not have any equivalent in uv. Previously, `migrate-to-uv` would warn about the issue, but still perform the migration. It now aborts the migration in case it is not able to perform the migration that would result in behavior changes when migrating to uv. 42 | 43 | If errors occur and lead to aborting the migration, you are expected to manually update your setup and retry the migration. 44 | 45 | Warnings that occurred during the migration (which did not break the behavior) are now also grouped and displayed at the very end of the migration. 46 | 47 | ### Features 48 | 49 | * feat!: abort on unrecoverable errors and warn about recoverable ones ([#480](https://github.com/mkniewallner/migrate-to-uv/pull/480)) 50 | * [poetry] Do not set optional groups as default ones ([#299](https://github.com/mkniewallner/migrate-to-uv/pull/299)) 51 | * Indicate support for Python 3.14 ([#468](https://github.com/mkniewallner/migrate-to-uv/pull/468)) 52 | 53 | ### Bug fixes 54 | 55 | * [poetry] Use inclusion when converting `^x.y` versions ([#466](https://github.com/mkniewallner/migrate-to-uv/pull/466)) 56 | * [poetry] Properly convert `include` to Hatch's build backend ([#477](https://github.com/mkniewallner/migrate-to-uv/pull/477)) 57 | * [poetry] Do not crash on empty `readme` array ([#481](https://github.com/mkniewallner/migrate-to-uv/pull/481)) 58 | * [poetry] Abort migration on dependencies using `||` operator, as there is no PEP 440 equivalent ([#487](https://github.com/mkniewallner/migrate-to-uv/pull/487)) 59 | * [poetry] Handle versions that use Poetry style and `,`, like `^1.0,!=1.1.0` ([#489](https://github.com/mkniewallner/migrate-to-uv/pull/489)) 60 | * [pip/pip-tools] Suggest how to add dependencies that could not be converted ([#350](https://github.com/mkniewallner/migrate-to-uv/pull/350)) 61 | * Preserve comments for sections unrelated to migration ([#471](https://github.com/mkniewallner/migrate-to-uv/pull/471)) 62 | 63 | ## 0.7.3 - 2025-06-07 64 | 65 | ### Bug fixes 66 | 67 | * Use correct `include-group` name to include groups when using `--dependency-groups-strategy include-in-dev` ([#283](https://github.com/mkniewallner/migrate-to-uv/pull/283)) 68 | * [poetry] Handle `=` single equality ([#288](https://github.com/mkniewallner/migrate-to-uv/pull/288)) 69 | * [pipenv] Handle raw versions ([#292](https://github.com/mkniewallner/migrate-to-uv/pull/292)) 70 | 71 | ## 0.7.2 - 2025-03-25 72 | 73 | ### Bug fixes 74 | 75 | * [pipenv] Handle `*` for version ([#212](https://github.com/mkniewallner/migrate-to-uv/pull/212)) 76 | 77 | ## 0.7.1 - 2025-02-22 78 | 79 | ### Bug fixes 80 | 81 | * Handle map for PEP 621 `license` field ([#156](https://github.com/mkniewallner/migrate-to-uv/pull/156)) 82 | 83 | ## 0.7.0 - 2025-02-15 84 | 85 | ### Features 86 | 87 | * Add `--skip-uv-checks` to skip checking if uv is already used in a project ([#118](https://github.com/mkniewallner/migrate-to-uv/pull/118)) 88 | 89 | ### Bug fixes 90 | 91 | * [pip/pip-tools] Warn on unhandled dependency formats ([#103](https://github.com/mkniewallner/migrate-to-uv/pull/103)) 92 | * [pip/pip-tools] Ignore inline comments when parsing dependencies ([#105](https://github.com/mkniewallner/migrate-to-uv/pull/105)) 93 | * [poetry] Migrate scripts that use `scripts = { callable = "foo:run" }` format instead of crashing ([#138](https://github.com/mkniewallner/migrate-to-uv/pull/138)) 94 | 95 | ## 0.6.0 - 2025-01-20 96 | 97 | Existing data in `[project]` section of `pyproject.toml` is now preserved by default when migrating. If you prefer that the section is fully replaced, this can be done by setting `--replace-project-section` flag, like so: 98 | 99 | ```bash 100 | migrate-to-uv --replace-project-section 101 | ``` 102 | 103 | Poetry projects that use PEP 621 syntax to define project metadata, for which support was added in [Poetry 2.0](https://python-poetry.org/blog/announcing-poetry-2.0.0/), are now supported. 104 | 105 | ### Features 106 | 107 | * Preserve existing data in `[project]` section of `pyproject.toml` when migrating ([#84](https://github.com/mkniewallner/migrate-to-uv/pull/84)) 108 | * [poetry] Support migrating projects using PEP 621 ([#85](https://github.com/mkniewallner/migrate-to-uv/pull/85)) 109 | 110 | ## 0.5.0 - 2025-01-18 111 | 112 | ### Features 113 | 114 | * [poetry] Delete `poetry.toml` after migration ([#62](https://github.com/mkniewallner/migrate-to-uv/pull/62)) 115 | * [pipenv] Delete `Pipfile.lock` after migration ([#66](https://github.com/mkniewallner/migrate-to-uv/pull/66)) 116 | * Exit if uv is detected as a package manager ([#61](https://github.com/mkniewallner/migrate-to-uv/pull/61)) 117 | 118 | ### Bug fixes 119 | 120 | * Ensure that lock file exists before parsing ([#67](https://github.com/mkniewallner/migrate-to-uv/pull/67)) 121 | 122 | ### Documentation 123 | 124 | * Explain how to set credentials for private indexes ([#60](https://github.com/mkniewallner/migrate-to-uv/pull/60)) 125 | 126 | ## 0.4.0 - 2025-01-17 127 | 128 | When generating `uv.lock` with `uv lock` command, `migrate-to-uv` now keeps the same versions dependencies were locked to with the previous package manager (if a lock file was found), both for direct and transitive dependencies. This is supported for Poetry, Pipenv, and pip-tools. 129 | 130 | This new behavior can be opted out by setting `--ignore-locked-versions` flag, like so: 131 | 132 | ```bash 133 | migrate-to-uv --ignore-locked-versions 134 | ``` 135 | 136 | ### Features 137 | 138 | * Keep locked dependencies versions when generating `uv.lock` ([#56](https://github.com/mkniewallner/migrate-to-uv/pull/56)) 139 | 140 | ## 0.3.0 - 2025-01-12 141 | 142 | Dependencies are now locked with `uv lock` at the end of the migration, if `uv` is detected as an executable. This new behavior can be opted out by setting `--skip-lock` flag, like so: 143 | 144 | ```bash 145 | migrate-to-uv --skip-lock 146 | ``` 147 | 148 | ### Features 149 | 150 | * Lock dependencies at the end of migration ([#46](https://github.com/mkniewallner/migrate-to-uv/pull/46)) 151 | 152 | ## 0.2.1 - 2025-01-05 153 | 154 | ### Bug fixes 155 | 156 | * [poetry] Avoid crashing when an extra lists a non-existing dependency ([#30](https://github.com/mkniewallner/migrate-to-uv/pull/30)) 157 | 158 | ## 0.2.0 - 2025-01-05 159 | 160 | ### Features 161 | 162 | * Support migrating projects using `pip` and `pip-tools` ([#24](https://github.com/mkniewallner/migrate-to-uv/pull/24)) 163 | * [poetry] Migrate data from `packages`, `include` and `exclude` to Hatch build backend ([#16](https://github.com/mkniewallner/migrate-to-uv/pull/16)) 164 | 165 | ## 0.1.2 - 2025-01-02 166 | 167 | ### Bug fixes 168 | 169 | * [pipenv] Correctly update `pyproject.toml` ([#19](https://github.com/mkniewallner/migrate-to-uv/pull/19)) 170 | * Do not insert `[tool.uv]` if empty ([#17](https://github.com/mkniewallner/migrate-to-uv/pull/17)) 171 | 172 | ## 0.1.1 - 2024-12-26 173 | 174 | ### Miscellaneous 175 | 176 | * Fix documentation publishing and package metadata ([#3](https://github.com/mkniewallner/migrate-to-uv/pull/3)) 177 | 178 | ## 0.1.0 - 2024-12-26 179 | 180 | Initial release, with support for Poetry and Pipenv. 181 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | --- 2 | icon: lucide/scroll-text 3 | --- 4 | # Changelog 5 | 6 | ## 0.9.1 - 2025-12-24 7 | 8 | ### Bug fixes 9 | 10 | * [poetry] Fail migration on missing `__init__.py` for uv build backend ([#553](https://github.com/mkniewallner/migrate-to-uv/pull/553)) 11 | 12 | ## 0.9.0 - 2025-12-22 13 | 14 | ### New features 15 | 16 | #### Experimental support for uv build backend 17 | 18 | When migrating Poetry package distribution metadata, `migrate-to-uv` uses [Hatch](https://hatch.pypa.io/latest/config/build/) build backend. Experimental support for migrating to uv build backend has been added behind `--build backend-uv` argument. 19 | 20 | Note that uv build backend offers less flexibility than Poetry and Hatch, so the migration might be aborted if some options used by Poetry cannot be expressed with uv build backend. If you try `--build-backend uv` and encounter any issue, feel free to report it. 21 | 22 | ### Features 23 | 24 | * [poetry] Add experimental `--build-backend uv` argument to migrate package distribution metadata to uv build backend ([#533](https://github.com/mkniewallner/migrate-to-uv/pull/533)) 25 | 26 | ### Bug fixes 27 | 28 | * [poetry] Do not migrate `packages`/`include` with empty `format` array ([#538](https://github.com/mkniewallner/migrate-to-uv/pull/538)) 29 | * [poetry] Fail migration on unhandled python marker specification ([#544](https://github.com/mkniewallner/migrate-to-uv/pull/544)) 30 | 31 | ## 0.8.1 - 2025-12-13 32 | 33 | ### Bug fixes 34 | 35 | * [poetry] Fail on unhandled version specifications ([#514](https://github.com/mkniewallner/migrate-to-uv/pull/514)) 36 | * [poetry] Also check if `poetry.lock` exists when checking if a project uses Poetry ([#528](https://github.com/mkniewallner/migrate-to-uv/pull/528)) 37 | 38 | ## 0.8.0 - 2025-11-17 39 | 40 | ### Breaking changes 41 | 42 | #### Abort on unrecoverable errors and warn about recoverable ones 43 | 44 | Although `migrate-to-uv` tries its best to migrate a project to uv without changing the behavior, some things that are accepted by package managers do not have any equivalent in uv. Previously, `migrate-to-uv` would warn about the issue, but still perform the migration. It now aborts the migration in case it is not able to perform the migration that would result in behavior changes when migrating to uv. 45 | 46 | If errors occur and lead to aborting the migration, you are expected to manually update your setup and retry the migration. 47 | 48 | Warnings that occurred during the migration (which did not break the behavior) are now also grouped and displayed at the very end of the migration. 49 | 50 | ### Features 51 | 52 | * feat!: abort on unrecoverable errors and warn about recoverable ones ([#480](https://github.com/mkniewallner/migrate-to-uv/pull/480)) 53 | * [poetry] Do not set optional groups as default ones ([#299](https://github.com/mkniewallner/migrate-to-uv/pull/299)) 54 | * Indicate support for Python 3.14 ([#468](https://github.com/mkniewallner/migrate-to-uv/pull/468)) 55 | 56 | ### Bug fixes 57 | 58 | * [poetry] Use inclusion when converting `^x.y` versions ([#466](https://github.com/mkniewallner/migrate-to-uv/pull/466)) 59 | * [poetry] Properly convert `include` to Hatch's build backend ([#477](https://github.com/mkniewallner/migrate-to-uv/pull/477)) 60 | * [poetry] Do not crash on empty `readme` array ([#481](https://github.com/mkniewallner/migrate-to-uv/pull/481)) 61 | * [poetry] Abort migration on dependencies using `||` operator, as there is no PEP 440 equivalent ([#487](https://github.com/mkniewallner/migrate-to-uv/pull/487)) 62 | * [poetry] Handle versions that use Poetry style and `,`, like `^1.0,!=1.1.0` ([#489](https://github.com/mkniewallner/migrate-to-uv/pull/489)) 63 | * [pip/pip-tools] Suggest how to add dependencies that could not be converted ([#350](https://github.com/mkniewallner/migrate-to-uv/pull/350)) 64 | * Preserve comments for sections unrelated to migration ([#471](https://github.com/mkniewallner/migrate-to-uv/pull/471)) 65 | 66 | ## 0.7.3 - 2025-06-07 67 | 68 | ### Bug fixes 69 | 70 | * Use correct `include-group` name to include groups when using `--dependency-groups-strategy include-in-dev` ([#283](https://github.com/mkniewallner/migrate-to-uv/pull/283)) 71 | * [poetry] Handle `=` single equality ([#288](https://github.com/mkniewallner/migrate-to-uv/pull/288)) 72 | * [pipenv] Handle raw versions ([#292](https://github.com/mkniewallner/migrate-to-uv/pull/292)) 73 | 74 | ## 0.7.2 - 2025-03-25 75 | 76 | ### Bug fixes 77 | 78 | * [pipenv] Handle `*` for version ([#212](https://github.com/mkniewallner/migrate-to-uv/pull/212)) 79 | 80 | ## 0.7.1 - 2025-02-22 81 | 82 | ### Bug fixes 83 | 84 | * Handle map for PEP 621 `license` field ([#156](https://github.com/mkniewallner/migrate-to-uv/pull/156)) 85 | 86 | ## 0.7.0 - 2025-02-15 87 | 88 | ### Features 89 | 90 | * Add `--skip-uv-checks` to skip checking if uv is already used in a project ([#118](https://github.com/mkniewallner/migrate-to-uv/pull/118)) 91 | 92 | ### Bug fixes 93 | 94 | * [pip/pip-tools] Warn on unhandled dependency formats ([#103](https://github.com/mkniewallner/migrate-to-uv/pull/103)) 95 | * [pip/pip-tools] Ignore inline comments when parsing dependencies ([#105](https://github.com/mkniewallner/migrate-to-uv/pull/105)) 96 | * [poetry] Migrate scripts that use `scripts = { callable = "foo:run" }` format instead of crashing ([#138](https://github.com/mkniewallner/migrate-to-uv/pull/138)) 97 | 98 | ## 0.6.0 - 2025-01-20 99 | 100 | Existing data in `[project]` section of `pyproject.toml` is now preserved by default when migrating. If you prefer that the section is fully replaced, this can be done by setting `--replace-project-section` flag, like so: 101 | 102 | ```bash 103 | migrate-to-uv --replace-project-section 104 | ``` 105 | 106 | Poetry projects that use PEP 621 syntax to define project metadata, for which support was added in [Poetry 2.0](https://python-poetry.org/blog/announcing-poetry-2.0.0/), are now supported. 107 | 108 | ### Features 109 | 110 | * Preserve existing data in `[project]` section of `pyproject.toml` when migrating ([#84](https://github.com/mkniewallner/migrate-to-uv/pull/84)) 111 | * [poetry] Support migrating projects using PEP 621 ([#85](https://github.com/mkniewallner/migrate-to-uv/pull/85)) 112 | 113 | ## 0.5.0 - 2025-01-18 114 | 115 | ### Features 116 | 117 | * [poetry] Delete `poetry.toml` after migration ([#62](https://github.com/mkniewallner/migrate-to-uv/pull/62)) 118 | * [pipenv] Delete `Pipfile.lock` after migration ([#66](https://github.com/mkniewallner/migrate-to-uv/pull/66)) 119 | * Exit if uv is detected as a package manager ([#61](https://github.com/mkniewallner/migrate-to-uv/pull/61)) 120 | 121 | ### Bug fixes 122 | 123 | * Ensure that lock file exists before parsing ([#67](https://github.com/mkniewallner/migrate-to-uv/pull/67)) 124 | 125 | ### Documentation 126 | 127 | * Explain how to set credentials for private indexes ([#60](https://github.com/mkniewallner/migrate-to-uv/pull/60)) 128 | 129 | ## 0.4.0 - 2025-01-17 130 | 131 | When generating `uv.lock` with `uv lock` command, `migrate-to-uv` now keeps the same versions dependencies were locked to with the previous package manager (if a lock file was found), both for direct and transitive dependencies. This is supported for Poetry, Pipenv, and pip-tools. 132 | 133 | This new behavior can be opted out by setting `--ignore-locked-versions` flag, like so: 134 | 135 | ```bash 136 | migrate-to-uv --ignore-locked-versions 137 | ``` 138 | 139 | ### Features 140 | 141 | * Keep locked dependencies versions when generating `uv.lock` ([#56](https://github.com/mkniewallner/migrate-to-uv/pull/56)) 142 | 143 | ## 0.3.0 - 2025-01-12 144 | 145 | Dependencies are now locked with `uv lock` at the end of the migration, if `uv` is detected as an executable. This new behavior can be opted out by setting `--skip-lock` flag, like so: 146 | 147 | ```bash 148 | migrate-to-uv --skip-lock 149 | ``` 150 | 151 | ### Features 152 | 153 | * Lock dependencies at the end of migration ([#46](https://github.com/mkniewallner/migrate-to-uv/pull/46)) 154 | 155 | ## 0.2.1 - 2025-01-05 156 | 157 | ### Bug fixes 158 | 159 | * [poetry] Avoid crashing when an extra lists a non-existing dependency ([#30](https://github.com/mkniewallner/migrate-to-uv/pull/30)) 160 | 161 | ## 0.2.0 - 2025-01-05 162 | 163 | ### Features 164 | 165 | * Support migrating projects using `pip` and `pip-tools` ([#24](https://github.com/mkniewallner/migrate-to-uv/pull/24)) 166 | * [poetry] Migrate data from `packages`, `include` and `exclude` to Hatch build backend ([#16](https://github.com/mkniewallner/migrate-to-uv/pull/16)) 167 | 168 | ## 0.1.2 - 2025-01-02 169 | 170 | ### Bug fixes 171 | 172 | * [pipenv] Correctly update `pyproject.toml` ([#19](https://github.com/mkniewallner/migrate-to-uv/pull/19)) 173 | * Do not insert `[tool.uv]` if empty ([#17](https://github.com/mkniewallner/migrate-to-uv/pull/17)) 174 | 175 | ## 0.1.1 - 2024-12-26 176 | 177 | ### Miscellaneous 178 | 179 | * Fix documentation publishing and package metadata ([#3](https://github.com/mkniewallner/migrate-to-uv/pull/3)) 180 | 181 | ## 0.1.0 - 2024-12-26 182 | 183 | Initial release, with support for Poetry and Pipenv. 184 | -------------------------------------------------------------------------------- /src/converters/poetry/version.rs: -------------------------------------------------------------------------------- 1 | use owo_colors::OwoColorize; 2 | use pep440_rs::{Version, VersionSpecifiers}; 3 | use std::str::FromStr; 4 | 5 | pub enum PoetryPep440 { 6 | String(String), 7 | Compatible(Version), 8 | Inclusive(Version, Version), 9 | } 10 | 11 | impl PoetryPep440 { 12 | pub fn to_python_marker(&self) -> String { 13 | let pep_440_python = VersionSpecifiers::from_str(self.to_string().as_str()).unwrap(); 14 | 15 | pep_440_python 16 | .iter() 17 | .map(|spec| format!("python_version {} '{}'", spec.operator(), spec.version())) 18 | .collect::>() 19 | .join(" and ") 20 | } 21 | 22 | /// 23 | fn from_caret(s: &str) -> Result { 24 | if let Ok(version) = Version::from_str(s) { 25 | return match version.clone().release() { 26 | [0, 0, z] => Ok(Self::Inclusive(version, Version::new([0, 0, z + 1]))), 27 | [0, y] | [0, y, _, ..] => Ok(Self::Inclusive(version, Version::new([0, y + 1]))), 28 | [x, _, ..] | [x] => Ok(Self::Inclusive(version, Version::new([x + 1]))), 29 | [..] => Ok(Self::String(String::new())), 30 | }; 31 | } 32 | Err(ParseVersionError::new( 33 | ParseVersionErrorKind::Other, 34 | s.to_string(), 35 | )) 36 | } 37 | 38 | /// 39 | fn from_tilde(s: &str) -> Result { 40 | if let Ok(version) = Version::from_str(s) { 41 | return match version.clone().release() { 42 | [_, _, _, ..] => Ok(Self::Compatible(version)), 43 | [x, y] => Ok(Self::Inclusive(version, Version::new([x, &(y + 1)]))), 44 | [x] => Ok(Self::Inclusive(version, Version::new([x + 1]))), 45 | [..] => Ok(Self::String(String::new())), 46 | }; 47 | } 48 | Err(ParseVersionError::new( 49 | ParseVersionErrorKind::Other, 50 | s.to_string(), 51 | )) 52 | } 53 | } 54 | 55 | #[derive(Debug, PartialEq, Eq)] 56 | pub enum ParseVersionErrorKind { 57 | OrOperator(String), 58 | Other, 59 | PythonMarkerOrOperator(String), 60 | PythonMarker, 61 | } 62 | 63 | #[derive(Debug, PartialEq, Eq)] 64 | pub struct ParseVersionError { 65 | pub kind: ParseVersionErrorKind, 66 | pub version: String, 67 | } 68 | 69 | impl ParseVersionError { 70 | pub fn new(kind: ParseVersionErrorKind, version: String) -> Self { 71 | Self { kind, version } 72 | } 73 | 74 | pub fn format(self, dependency: &str) -> String { 75 | match self.kind { 76 | ParseVersionErrorKind::OrOperator(operator) => { 77 | format!( 78 | "\"{}\" dependency with version \"{}\" contains \"{}\", which is specific to Poetry and not supported by PEP 440. See https://mkniewallner.github.io/migrate-to-uv/supported-package-managers/#operator for guidance.", 79 | dependency.bold(), 80 | self.version.bold(), 81 | operator.bold(), 82 | ) 83 | } 84 | ParseVersionErrorKind::PythonMarkerOrOperator(operator) => { 85 | format!( 86 | "\"{}\" dependency with python marker \"{}\" contains \"{}\", which is specific to Poetry and not supported by PEP 440. See https://mkniewallner.github.io/migrate-to-uv/supported-package-managers/#operator for guidance.", 87 | dependency.bold(), 88 | self.version.bold(), 89 | operator.bold(), 90 | ) 91 | } 92 | ParseVersionErrorKind::PythonMarker => { 93 | format!( 94 | "\"{}\" dependency with python marker \"{}\" could not be transformed to PEP 440 format. Make sure to check https://mkniewallner.github.io/migrate-to-uv/supported-package-managers/#unsupported-version-specifiers.", 95 | dependency.bold(), 96 | self.version.bold(), 97 | ) 98 | } 99 | ParseVersionErrorKind::Other => { 100 | format!( 101 | "\"{}\" dependency with version \"{}\" could not be transformed to PEP 440 format. Make sure to check https://mkniewallner.github.io/migrate-to-uv/supported-package-managers/#unsupported-version-specifiers.", 102 | dependency.bold(), 103 | self.version.bold(), 104 | ) 105 | } 106 | } 107 | } 108 | } 109 | 110 | impl FromStr for PoetryPep440 { 111 | type Err = ParseVersionError; 112 | 113 | fn from_str(s: &str) -> Result { 114 | // While Poetry has its own specification for version specifiers, it also supports most of 115 | // the version specifiers defined by PEP 440. So if the version is a valid PEP 440 116 | // definition, we can directly use it without any transformation. 117 | if VersionSpecifiers::from_str(s).is_ok() { 118 | return Ok(Self::String(s.to_string())); 119 | } 120 | 121 | // Poetry supports an `||` operator (or `|`, which is equivalent), as in 122 | // "^1.0 || ^2.0 || ^3.0", but there is no PEP 440 equivalent, so it cannot be converted. 123 | for operator in ["||", "|"] { 124 | if s.contains(operator) { 125 | return Err(ParseVersionError::new( 126 | ParseVersionErrorKind::OrOperator(operator.to_string()), 127 | s.to_string(), 128 | )); 129 | } 130 | } 131 | 132 | let mut pep_440_specifier = Vec::new(); 133 | 134 | // Even when using Poetry-specific version specifiers, it is still possible to define 135 | // additional PEP 440 specifiers (e.g., "^1.0,!=1.1.0") or even define multiple Poetry 136 | // specifiers (e.g., "^1.0,^1.1"), so we need to split over "," and treat each group 137 | // separately, knowing that each group can either be a Poetry-specific specifier, or a PEP 138 | // 440 one. 139 | for specifier in s.trim().split(',') { 140 | let specifier = specifier.trim(); 141 | 142 | // If the subgroup is a valid PEP 440 specifier, we can directly use it without any 143 | // transformation. 144 | if VersionSpecifiers::from_str(specifier).is_ok() { 145 | pep_440_specifier.push(Self::String(specifier.to_string())); 146 | } else { 147 | let mut chars = specifier.chars(); 148 | 149 | match (chars.next(), chars.as_str()) { 150 | (Some('*'), "") => pep_440_specifier.push(Self::String(String::new())), 151 | (Some('^'), version) => match Self::from_caret(version.trim()) { 152 | Ok(v) => pep_440_specifier.push(v), 153 | Err(e) => return Err(e), 154 | }, 155 | (Some('~'), version) => match Self::from_tilde(version.trim()) { 156 | Ok(v) => pep_440_specifier.push(v), 157 | Err(e) => return Err(e), 158 | }, 159 | (Some('='), version) => { 160 | pep_440_specifier.push(Self::String(format!("=={version}"))); 161 | } 162 | (Some('0'..='9'), _) => pep_440_specifier.push(Self::String(format!("=={s}"))), 163 | _ => { 164 | return Err(ParseVersionError::new( 165 | ParseVersionErrorKind::Other, 166 | s.to_string(), 167 | )); 168 | } 169 | } 170 | } 171 | } 172 | 173 | // Concatenate the different specifiers that were split over "," into the final version. 174 | let version = pep_440_specifier 175 | .iter() 176 | .map(ToString::to_string) 177 | .collect::>() 178 | .join(","); 179 | 180 | // At this point, we should end up with a PEP 440-valid version. If it's not the case, we 181 | // should error out. 182 | match VersionSpecifiers::from_str(version.as_str()) { 183 | Ok(_) => Ok(Self::String(version)), 184 | Err(_) => Err(ParseVersionError::new( 185 | ParseVersionErrorKind::Other, 186 | s.to_string(), 187 | )), 188 | } 189 | } 190 | } 191 | 192 | impl std::fmt::Display for PoetryPep440 { 193 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 194 | let str = match &self { 195 | Self::String(s) => s.clone(), 196 | Self::Compatible(version) => format!("~={version}"), 197 | Self::Inclusive(lower, upper) => format!(">={lower},<{upper}"), 198 | }; 199 | 200 | write!(f, "{str}") 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/converters/pipenv/dependencies.rs: -------------------------------------------------------------------------------- 1 | use crate::converters::{DependencyGroupsAndDefaultGroups, DependencyGroupsStrategy}; 2 | use crate::schema; 3 | use crate::schema::pipenv::{DependencySpecification, KeywordMarkers}; 4 | use crate::schema::pyproject::DependencyGroupSpecification; 5 | use crate::schema::uv::{SourceContainer, SourceIndex}; 6 | use indexmap::IndexMap; 7 | 8 | pub fn get( 9 | pipenv_dependencies: Option<&IndexMap>, 10 | uv_source_index: &mut IndexMap, 11 | ) -> Option> { 12 | Some( 13 | pipenv_dependencies? 14 | .iter() 15 | .map(|(name, specification)| { 16 | let source_index = match specification { 17 | DependencySpecification::Map { 18 | index: Some(index), .. 19 | } => Some(SourceContainer::SourceIndex(SourceIndex { 20 | index: Some(index.clone()), 21 | ..Default::default() 22 | })), 23 | DependencySpecification::Map { 24 | path: Some(path), 25 | editable, 26 | .. 27 | } => Some(SourceContainer::SourceIndex(SourceIndex { 28 | path: Some(path.clone()), 29 | editable: *editable, 30 | ..Default::default() 31 | })), 32 | DependencySpecification::Map { 33 | git: Some(git), 34 | ref_, 35 | .. 36 | } => Some(SourceContainer::SourceIndex(SourceIndex { 37 | git: Some(git.clone()), 38 | rev: ref_.clone(), 39 | ..Default::default() 40 | })), 41 | _ => None, 42 | }; 43 | 44 | if let Some(source_index) = source_index { 45 | uv_source_index.insert(name.clone(), source_index); 46 | } 47 | 48 | match specification { 49 | DependencySpecification::String(spec) => { 50 | if spec.as_str() == "*" { 51 | name.clone() 52 | } else { 53 | // Handle raw versions like "1.2.3", which, while undocumented, are also 54 | // valid for Pipenv. 55 | if spec.chars().next().unwrap_or_default().is_ascii_digit() { 56 | format!("{name}=={spec}") 57 | } else { 58 | format!("{name}{spec}") 59 | } 60 | } 61 | } 62 | DependencySpecification::Map { 63 | version, 64 | extras, 65 | markers, 66 | keyword_markers, 67 | .. 68 | } => { 69 | let mut pep_508_version = name.clone(); 70 | let mut combined_markers: Vec = 71 | get_keyword_markers(keyword_markers); 72 | 73 | if let Some(extras) = extras { 74 | pep_508_version.push_str(format!("[{}]", extras.join(", ")).as_str()); 75 | } 76 | 77 | if let Some(version) = version 78 | && version.as_str() != "*" 79 | { 80 | // Handle raw versions like "1.2.3", which, while undocumented, are 81 | // also valid for Pipenv. 82 | if version.chars().next().unwrap_or_default().is_ascii_digit() { 83 | pep_508_version.push_str("=="); 84 | } 85 | pep_508_version.push_str(version); 86 | } 87 | 88 | if let Some(markers) = markers { 89 | combined_markers.push(markers.clone()); 90 | } 91 | 92 | if !combined_markers.is_empty() { 93 | pep_508_version.push_str( 94 | format!(" ; {}", combined_markers.join(" and ")).as_str(), 95 | ); 96 | } 97 | 98 | pep_508_version.clone() 99 | } 100 | } 101 | }) 102 | .collect(), 103 | ) 104 | } 105 | 106 | fn get_keyword_markers(keyword_markers: &KeywordMarkers) -> Vec { 107 | let mut markers: Vec = Vec::new(); 108 | 109 | macro_rules! push_marker { 110 | ($field:expr, $name:expr) => { 111 | if let Some(value) = &$field { 112 | markers.push(format!("{} {}", $name, value)); 113 | } 114 | }; 115 | } 116 | 117 | push_marker!(keyword_markers.os_name, "os_name"); 118 | push_marker!(keyword_markers.sys_platform, "sys_platform"); 119 | push_marker!(keyword_markers.platform_machine, "platform_machine"); 120 | push_marker!( 121 | keyword_markers.platform_python_implementation, 122 | "platform_python_implementation" 123 | ); 124 | push_marker!(keyword_markers.platform_release, "platform_release"); 125 | push_marker!(keyword_markers.platform_system, "platform_system"); 126 | push_marker!(keyword_markers.platform_version, "platform_version"); 127 | push_marker!(keyword_markers.python_version, "python_version"); 128 | push_marker!(keyword_markers.python_full_version, "python_full_version"); 129 | push_marker!(keyword_markers.implementation_name, "implementation_name"); 130 | push_marker!( 131 | keyword_markers.implementation_version, 132 | "implementation_version" 133 | ); 134 | 135 | markers 136 | } 137 | 138 | pub fn get_dependency_groups_and_default_groups( 139 | pipfile: &schema::pipenv::Pipfile, 140 | uv_source_index: &mut IndexMap, 141 | dependency_groups_strategy: DependencyGroupsStrategy, 142 | ) -> DependencyGroupsAndDefaultGroups { 143 | let mut dependency_groups: IndexMap> = 144 | IndexMap::new(); 145 | let mut default_groups: Vec = Vec::new(); 146 | 147 | // Add dependencies from legacy `[dev-packages]` into `dev` dependency group. 148 | if let Some(dev_dependencies) = &pipfile.dev_packages { 149 | dependency_groups.insert( 150 | "dev".to_string(), 151 | get(Some(dev_dependencies), uv_source_index) 152 | .unwrap_or_default() 153 | .into_iter() 154 | .map(DependencyGroupSpecification::String) 155 | .collect(), 156 | ); 157 | } 158 | 159 | // Add dependencies from `[]` into `` dependency group, 160 | // unless `MergeIntoDev` strategy is used, in which case we add them into `dev` dependency 161 | // group. 162 | if let Some(category_group) = &pipfile.category_groups { 163 | for (group, dependency_specification) in category_group { 164 | dependency_groups 165 | .entry(match dependency_groups_strategy { 166 | DependencyGroupsStrategy::MergeIntoDev => "dev".to_string(), 167 | _ => group.clone(), 168 | }) 169 | .or_default() 170 | .extend( 171 | get(Some(dependency_specification), uv_source_index) 172 | .unwrap_or_default() 173 | .into_iter() 174 | .map(DependencyGroupSpecification::String), 175 | ); 176 | } 177 | 178 | match dependency_groups_strategy { 179 | // When using `SetDefaultGroups` strategy, all dependency groups are referenced in 180 | // `default-groups` under `[tool.uv]` section. If we only have `dev` dependency group, 181 | // do not set `default-groups`, as this is already uv's default. 182 | DependencyGroupsStrategy::SetDefaultGroups => { 183 | if !dependency_groups.keys().eq(["dev"]) { 184 | default_groups.extend(dependency_groups.keys().map(ToString::to_string)); 185 | } 186 | } 187 | // When using `IncludeInDev` strategy, dependency groups (except `dev` one) are 188 | // referenced from `dev` dependency group with `{ include-group = "" }`. 189 | DependencyGroupsStrategy::IncludeInDev => { 190 | dependency_groups 191 | .entry("dev".to_string()) 192 | .or_default() 193 | .extend(category_group.keys().filter(|&k| k != "dev").map(|g| { 194 | DependencyGroupSpecification::Map { 195 | include_group: Some(g.clone()), 196 | } 197 | })); 198 | } 199 | _ => (), 200 | } 201 | } 202 | 203 | if dependency_groups.is_empty() { 204 | return (None, None); 205 | } 206 | 207 | ( 208 | Some(dependency_groups), 209 | if default_groups.is_empty() { 210 | None 211 | } else { 212 | Some(default_groups) 213 | }, 214 | ) 215 | } 216 | -------------------------------------------------------------------------------- /docs/supported-package-managers.md: -------------------------------------------------------------------------------- 1 | --- 2 | icon: lucide/package 3 | --- 4 | # Supported package managers 5 | 6 | `migrate-to-uv` supports multiple package managers. By default, it tries to auto-detect the package manager based on the 7 | files (and their content) used by the package managers it supports. If you need to enforce a specific package manager to 8 | be used, use [`--package-manager`](configuration.md#-package-manager). 9 | 10 | ## Poetry 11 | 12 | !!! note 13 | 14 | `migrate-to-uv` supports migrating both projects that use Poetry-specific syntax for defining project metadata, and 15 | projects that use PEP 621, added in [Poetry 2.0](https://python-poetry.org/blog/announcing-poetry-2.0.0/). 16 | 17 | All existing [Poetry](https://python-poetry.org/) metadata should be converted to uv when performing the migration: 18 | 19 | - [Project metadata](https://python-poetry.org/docs/pyproject/) (`name`, `version`, `authors`, ...) 20 | - [Dependencies and dependency groups](https://python-poetry.org/docs/pyproject/#dependencies-and-dependency-groups) 21 | (PyPI, path, git, URL) 22 | - [Dependency extras](https://python-poetry.org/docs/pyproject/#extras) (also known as optional dependencies) 23 | - [Dependency sources](https://python-poetry.org/docs/repositories/) 24 | - [Dependency markers](https://python-poetry.org/docs/dependency-specification/#using-environment-markers) (including 25 | [`python`](https://python-poetry.org/docs/dependency-specification/#python-restricted-dependencies) and `platform`) 26 | - [Multiple constraints dependencies](https://python-poetry.org/docs/dependency-specification/#multiple-constraints-dependencies) 27 | - Package distribution metadata ([`packages`](https://python-poetry.org/docs/pyproject/#packages), [`include` and `exclude`](https://python-poetry.org/docs/pyproject/#exclude-and-include)) 28 | - [Supported Python versions](https://python-poetry.org/docs/basic-usage/#setting-a-python-version) 29 | - [Scripts](https://python-poetry.org/docs/pyproject/#scripts) and 30 | [plugins](https://python-poetry.org/docs/pyproject/#plugins) (also known as entry points) 31 | 32 | Version definitions set for dependencies are also preserved, and converted to their 33 | equivalent [PEP 440](https://peps.python.org/pep-0440/) format used by uv, even for Poetry-specific version 34 | specification (e.g., [caret](https://python-poetry.org/docs/dependency-specification/#caret-requirements) (`^`) 35 | and [tilde](https://python-poetry.org/docs/dependency-specification/#tilde-requirements) (`~`)). 36 | 37 | ### Unsupported version specifiers 38 | 39 | Although `migrate-to-uv` is able to migrate most Poetry-specific version specifiers to 40 | [PEP 440](https://packaging.python.org/en/latest/specifications/version-specifiers/#id5) format, some specific features 41 | of Poetry versioning cannot be migrated, either because there are no equivalent in PEP 440, or because some cases are 42 | too complex to handle. In both cases, `migrate-to-uv` will fail the migration. 43 | 44 | This section lists the features that are known to not be supported, and helps you find what to do to actually be able to 45 | perform the migration. If a case that `migrate-to-uv` was not able to handle is not listed here, feel free to create an 46 | issue on the GitHub repository. 47 | 48 | #### "||" operator 49 | 50 | Poetry allows defining multiple versions constraints using a double-pipe ("||") (or the equivalent pipe ("|")), that 51 | acts as an "or" operator, like this: 52 | 53 | ```toml 54 | [tool.poetry.dependencies] 55 | pytest = "^7.0||^8.0||^9.0" 56 | ``` 57 | 58 | Since [PEP 440](https://packaging.python.org/en/latest/specifications/version-specifiers/#id5) does not provide any way 59 | to define an "or" operator, usage of this operator in versions will lead to a migration failure. You will need to 60 | manually update the syntax to not rely on this operator before attempting the migration. 61 | 62 | In the simple example above, this one should be equivalent: 63 | 64 | ```toml 65 | [tool.poetry.dependencies] 66 | pytest = ">=7.0,<10" 67 | ``` 68 | 69 | For cases where specifiers are non-contiguous, you might want to only keep the highest version, e.g.: 70 | 71 | ```toml 72 | [tool.poetry.dependencies] 73 | pytest = "^7.0||^9.0" 74 | ``` 75 | 76 | could be transformed to: 77 | 78 | ```toml 79 | [tool.poetry.dependencies] 80 | pytest = "^9.0" 81 | ``` 82 | 83 | Note that non-contiguous versions that use "or" operator only work on projects that are not distributed as packages 84 | (since packages need to follow PEP 440 versioning). 85 | 86 | #### Whitespace-separated specifiers 87 | 88 | Similarly to [PEP 440](https://packaging.python.org/en/latest/specifications/version-specifiers/#id5), Poetry allows 89 | defining multiple versions constraints using a comma (","), that acts as an "and" operator, like this: 90 | 91 | ```toml 92 | [tool.poetry.dependencies] 93 | pytest = "^8.4,!=8.4.2" 94 | ``` 95 | 96 | While this one is supported by `migrate-to-uv`, Poetry also allows using a whitespace (" ") instead of a comma (which is 97 | equivalent), like this: 98 | 99 | ```toml 100 | [tool.poetry.dependencies] 101 | pytest = "^8.4 !=8.4.2" 102 | ``` 103 | 104 | Using whitespaces in place of commas for delimiting specifiers is not supported by `migrate-to-uv`, and will lead to a 105 | migration failure. You will need to manually update the syntax to use comma-delimited specifiers before attempting the 106 | migration. 107 | 108 | ### Build backend 109 | 110 | Although uv has [its own build backend](https://docs.astral.sh/uv/concepts/build-backend/), it cannot express everything 111 | that Poetry supports. Additionally, `migrate-to-uv` was created before uv build backend was stabilized, and 112 | chose [Hatch](https://hatch.pypa.io/latest/config/build/) in the meantime. 113 | 114 | !!! info 115 | 116 | Support for uv build backend was recently added, but is still considered experimental, so Hatch is still used by 117 | default. If you want to explicitly use uv as a build backend, you can 118 | use [`--build-backend uv`](configuration.md#-build-backend), but note that the migration can fail if you use some 119 | package distribution options that cannot be expressed with uv build backend. 120 | 121 | When converting the build backend to Hatch, `migrate-to-uv` migrates the following things: 122 | 123 | - Poetry [`packages`](https://python-poetry.org/docs/pyproject/#packages) and [`include`](https://python-poetry.org/docs/pyproject/#exclude-and-include) to Hatch [`include`](https://hatch.pypa.io/latest/config/build/#patterns) 124 | - Poetry [`exclude`](https://python-poetry.org/docs/pyproject/#exclude-and-include) to Hatch [`exclude`](https://hatch.pypa.io/latest/config/build/#patterns) 125 | 126 | !!! note 127 | 128 | Path rewriting, defined with `to` in `packages` for Poetry, is also migrated to Hatch by defining 129 | [sources](https://hatch.pypa.io/latest/config/build/#rewriting-paths) in wheel target. 130 | 131 | ## Pipenv 132 | 133 | All existing [Pipenv](https://pipenv.pypa.io/en/stable/) metadata should be converted to uv when performing the 134 | migration: 135 | 136 | - [Dependencies](https://pipenv.pypa.io/en/stable/pipfile.html#packages-section) and [development dependencies](https://pipenv.pypa.io/en/stable/pipfile.html#development-packages-section) (PyPI, 137 | path, git, URL) 138 | - [Package category groups](https://pipenv.pypa.io/en/stable/pipfile.html#custom-package-categories) 139 | - [Package indexes](https://pipenv.pypa.io/en/stable/indexes.html) 140 | - [Dependency markers](https://pipenv.pypa.io/en/stable/specifiers.html#advanced-version-specifiers) 141 | - [Supported Python versions](https://pipenv.pypa.io/en/stable/advanced.html#automatic-python-installation) 142 | 143 | ## pip-tools 144 | 145 | Most [pip-tools](https://pip-tools.readthedocs.io/en/stable/) metadata is converted to uv when performing the migration. 146 | 147 | By default, `migrate-to-uv` will search for: 148 | 149 | - production dependencies in `requirements.in` 150 | - development dependencies in `requirements-dev.in` 151 | 152 | If your project uses different file names, or defines production and/or development dependencies across multiple files, 153 | you can specify the names of the files using [`--requirements-file`](configuration.md#-requirements-file) and 154 | [`--dev-requirements-file`](configuration.md#-dev-requirements-file) (both can be specified multiple times), for 155 | instance: 156 | 157 | ```bash 158 | migrate-to-uv \ 159 | --requirements-file requirements-prod.in \ 160 | --dev-requirements-file requirements-dev.in \ 161 | --dev-requirements-file requirements-docs.in 162 | ``` 163 | 164 | ### Missing features 165 | 166 | - Dependencies that do not follow [PEP 508](https://peps.python.org/pep-0508/) specification are not yet handled 167 | - References to other requirement files (e.g., `-r other-requirements.in`) are not supported, but the requirements file 168 | can manually be set with [`--requirements-file`](configuration.md#-requirements-file) or 169 | [`--dev-requirements-file`](configuration.md#-dev-requirements-file) 170 | - Index URLs are not yet migrated 171 | 172 | ## pip 173 | 174 | Most [pip](https://pip.pypa.io/en/stable/) metadata is converted to uv when performing the migration. 175 | 176 | By default, `migrate-to-uv` will search for: 177 | 178 | - production dependencies in `requirements.txt` 179 | - development dependencies in `requirements-dev.txt` 180 | 181 | If your project uses different file names, or defines production and/or development dependencies across multiple files, 182 | you can specify the names of the files [`--requirements-file`](configuration.md#-requirements-file) and 183 | [`--dev-requirements-file`](configuration.md#-dev-requirements-file) (both can be specified multiple times), 184 | for instance: 185 | 186 | ```bash 187 | migrate-to-uv \ 188 | --requirements-file requirements-prod.txt \ 189 | --dev-requirements-file requirements-dev.txt \ 190 | --dev-requirements-file requirements-docs.txt 191 | ``` 192 | 193 | ### Missing features 194 | 195 | - Dependencies that do not follow [PEP 508](https://peps.python.org/pep-0508/) specification are not yet handled 196 | - References to other requirement files (e.g., `-r other-requirements.txt`) are not supported, but the requirements file 197 | can manually be set with [`--requirements-file`](configuration.md#-requirements-file) or 198 | [`--dev-requirements-file`](configuration.md#-dev-requirements-file) 199 | - Index URLs are not yet migrated 200 | -------------------------------------------------------------------------------- /src/converters/poetry/dependencies.rs: -------------------------------------------------------------------------------- 1 | use crate::converters::poetry::sources; 2 | use crate::converters::{DependencyGroupsAndDefaultGroups, DependencyGroupsStrategy}; 3 | use crate::errors::{add_recoverable_error, add_unrecoverable_error}; 4 | use crate::schema; 5 | use crate::schema::poetry::DependencySpecification; 6 | use crate::schema::pyproject::DependencyGroupSpecification; 7 | use crate::schema::uv::{SourceContainer, SourceIndex}; 8 | use indexmap::IndexMap; 9 | use owo_colors::OwoColorize; 10 | use std::collections::HashSet; 11 | 12 | pub fn get( 13 | poetry_dependencies: Option<&IndexMap>, 14 | uv_source_index: &mut IndexMap, 15 | ) -> Option> { 16 | let poetry_dependencies = poetry_dependencies?; 17 | let mut dependencies: Vec = Vec::new(); 18 | 19 | for (name, specification) in poetry_dependencies { 20 | match specification { 21 | DependencySpecification::String(_) => match specification.to_pep_508() { 22 | Ok(v) => dependencies.push(format!("{name}{v}")), 23 | Err(e) => add_unrecoverable_error(e.format(name)), 24 | }, 25 | DependencySpecification::Map { .. } => { 26 | let source_index = sources::get_source_index(specification); 27 | 28 | if let Some(source_index) = source_index { 29 | uv_source_index 30 | .insert(name.clone(), SourceContainer::SourceIndex(source_index)); 31 | } 32 | 33 | match specification.to_pep_508() { 34 | Ok(v) => dependencies.push(format!("{name}{v}")), 35 | Err(e) => add_unrecoverable_error(e.format(name)), 36 | } 37 | } 38 | // Multiple constraints dependencies: https://python-poetry.org/docs/dependency-specification#multiple-constraints-dependencies 39 | DependencySpecification::Vec(specs) => { 40 | let mut source_indexes: Vec = Vec::new(); 41 | 42 | for spec in specs { 43 | let source_index = sources::get_source_index(spec); 44 | 45 | // When using multiple constraints and a source is set, markers apply to the 46 | // source, not the dependency. So if we find both a source and a marker, we 47 | // apply the marker to the source. 48 | if let Some(mut source_index) = source_index { 49 | if let DependencySpecification::Map { 50 | python, 51 | platform, 52 | markers, 53 | .. 54 | } = spec 55 | && (python.is_some() || platform.is_some() || markers.is_some()) 56 | { 57 | match spec.get_marker() { 58 | Ok(marker) => source_index.marker = marker, 59 | Err(e) => add_unrecoverable_error(e.format(name)), 60 | } 61 | } 62 | 63 | source_indexes.push(source_index); 64 | } 65 | } 66 | 67 | // If no source was found on any of the dependency specification, we add the 68 | // different variants of the dependencies with their respective markers. Otherwise, 69 | // we add the different variants of the sources with their respective markers. 70 | if source_indexes.is_empty() { 71 | for spec in specs { 72 | match spec.to_pep_508() { 73 | Ok(v) => dependencies.push(format!("{name}{v}")), 74 | Err(e) => add_unrecoverable_error(e.format(name)), 75 | } 76 | } 77 | } else { 78 | uv_source_index 79 | .insert(name.clone(), SourceContainer::SourceIndexes(source_indexes)); 80 | 81 | dependencies.push(name.clone()); 82 | } 83 | } 84 | } 85 | } 86 | 87 | if dependencies.is_empty() { 88 | return None; 89 | } 90 | 91 | Some(dependencies) 92 | } 93 | 94 | pub fn get_optional( 95 | poetry_dependencies: &mut Option>, 96 | extras: Option>>, 97 | ) -> Option>> { 98 | let extras = extras?; 99 | let poetry_dependencies = poetry_dependencies.as_mut()?; 100 | 101 | let mut dependencies_to_remove: HashSet<&str> = HashSet::new(); 102 | 103 | let optional_dependencies: IndexMap> = extras 104 | .iter() 105 | .map(|(extra, extra_dependencies)| { 106 | ( 107 | extra.clone(), 108 | extra_dependencies 109 | .iter() 110 | .filter_map(|dependency| { 111 | // If dependency listed in extra does not exist, warn the user. 112 | poetry_dependencies.get(dependency).map_or_else( 113 | || { 114 | add_recoverable_error(format!( 115 | "Could not find dependency \"{}\" listed in \"{}\" extra.", 116 | dependency.bold(), 117 | extra.bold() 118 | )); 119 | None 120 | }, 121 | |dependency_specification| { 122 | dependencies_to_remove.insert(dependency); 123 | Some(format!( 124 | "{}{}", 125 | dependency, 126 | dependency_specification.to_pep_508().unwrap(), 127 | )) 128 | }, 129 | ) 130 | }) 131 | .collect(), 132 | ) 133 | }) 134 | .collect(); 135 | 136 | if optional_dependencies.is_empty() { 137 | return None; 138 | } 139 | 140 | for dep in dependencies_to_remove { 141 | let _ = &mut poetry_dependencies.shift_remove(dep); 142 | } 143 | 144 | Some(optional_dependencies) 145 | } 146 | 147 | pub fn get_dependency_groups_and_default_groups( 148 | poetry: &schema::poetry::Poetry, 149 | uv_source_index: &mut IndexMap, 150 | dependency_groups_strategy: DependencyGroupsStrategy, 151 | ) -> DependencyGroupsAndDefaultGroups { 152 | let mut dependency_groups: IndexMap> = 153 | IndexMap::new(); 154 | let mut default_groups: Vec = Vec::new(); 155 | 156 | // Add dependencies from legacy `[poetry.dev-dependencies]` into `dev` dependency group. 157 | if let Some(dev_dependencies) = &poetry.dev_dependencies { 158 | dependency_groups.insert( 159 | "dev".to_string(), 160 | get(Some(dev_dependencies), uv_source_index) 161 | .unwrap_or_default() 162 | .into_iter() 163 | .map(DependencyGroupSpecification::String) 164 | .collect(), 165 | ); 166 | } 167 | 168 | // Add dependencies from `[poetry.group..dependencies]` into `` dependency group, 169 | // unless `MergeIntoDev` strategy is used, in which case: 170 | // - we add non-optional groups into `dev` dependency group 171 | // - we keep the original group for optional groups 172 | if let Some(poetry_group) = &poetry.group { 173 | let mut optional_groups = HashSet::new(); 174 | 175 | for (group, dependency_group) in poetry_group { 176 | if dependency_group.optional == Some(true) { 177 | optional_groups.insert(group.clone()); 178 | } 179 | 180 | dependency_groups 181 | .entry(match dependency_groups_strategy { 182 | DependencyGroupsStrategy::MergeIntoDev if !optional_groups.contains(group) => { 183 | "dev".to_string() 184 | } 185 | _ => group.clone(), 186 | }) 187 | .or_default() 188 | .extend( 189 | get(Some(&dependency_group.dependencies), uv_source_index) 190 | .unwrap_or_default() 191 | .into_iter() 192 | .map(DependencyGroupSpecification::String), 193 | ); 194 | } 195 | 196 | match dependency_groups_strategy { 197 | // When using `SetDefaultGroups` strategy, all non-optional dependency groups are 198 | // referenced in `default-groups` under `[tool.uv]` section. If we only have `dev` 199 | // dependency group, do not set `default-groups`, as this is already uv's default. 200 | DependencyGroupsStrategy::SetDefaultGroups => { 201 | if !dependency_groups.keys().eq(["dev"]) { 202 | default_groups.extend( 203 | dependency_groups 204 | .keys() 205 | .filter(|&group| !optional_groups.contains(group)) 206 | .map(ToString::to_string), 207 | ); 208 | } 209 | } 210 | // When using `IncludeInDev` strategy, non-optional dependency groups (except `dev` one) 211 | // are referenced from `dev` dependency group with `{ include-group = "" }`. 212 | DependencyGroupsStrategy::IncludeInDev => { 213 | dependency_groups 214 | .entry("dev".to_string()) 215 | .or_default() 216 | .extend( 217 | poetry_group 218 | .keys() 219 | .filter(|&k| k != "dev" && !optional_groups.contains(k)) 220 | .map(|g| DependencyGroupSpecification::Map { 221 | include_group: Some(g.clone()), 222 | }), 223 | ); 224 | } 225 | _ => (), 226 | } 227 | } 228 | 229 | if dependency_groups.is_empty() { 230 | return (None, None); 231 | } 232 | 233 | ( 234 | Some(dependency_groups), 235 | if default_groups.is_empty() { 236 | None 237 | } else { 238 | Some(default_groups) 239 | }, 240 | ) 241 | } 242 | --------------------------------------------------------------------------------