From 6244f1b6b2ce35ece487bb9cfe266a03105228ef Mon Sep 17 00:00:00 2001 From: Mathieu Kniewallner Date: Mon, 20 Jan 2025 14:14:38 +0100 Subject: [PATCH 1/2] feat(pep621): flatten additional fields --- src/converters/poetry/mod.rs | 1 + src/schema/pep_621.rs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/converters/poetry/mod.rs b/src/converters/poetry/mod.rs index 5648c43..9cb71a7 100644 --- a/src/converters/poetry/mod.rs +++ b/src/converters/poetry/mod.rs @@ -83,6 +83,7 @@ impl Converter for Poetry { scripts: project::get_scripts(poetry.scripts, scripts_from_plugins), gui_scripts, entry_points: poetry_plugins, + ..Default::default() }; let uv = Uv { diff --git a/src/schema/pep_621.rs b/src/schema/pep_621.rs index 5ffa5c5..11ec2fe 100644 --- a/src/schema/pep_621.rs +++ b/src/schema/pep_621.rs @@ -2,6 +2,8 @@ use crate::schema::poetry::Poetry; use crate::schema::uv::Uv; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; /// #[derive(Default, Deserialize, Serialize)] @@ -26,6 +28,8 @@ pub struct Project { pub gui_scripts: Option>, #[serde(rename = "entry-points")] pub entry_points: Option>>, + #[serde(flatten)] + pub remaining_fields: HashMap, } #[derive(Deserialize, Serialize)] From be4b4e6f860970c4a2f0f63afa60bec2528c9b29 Mon Sep 17 00:00:00 2001 From: Mathieu Kniewallner Date: Mon, 20 Jan 2025 14:42:54 +0100 Subject: [PATCH 2/2] feat(converters): preserve existing data in `[project]` --- CHANGELOG.md | 12 +++ docs/usage-and-configuration.md | 11 +++ src/cli.rs | 6 ++ src/converters/mod.rs | 42 ++++++++++ src/converters/pip/mod.rs | 11 ++- src/converters/pipenv/mod.rs | 9 +- src/converters/poetry/mod.rs | 3 +- src/detector.rs | 1 + .../pip/existing_project/pyproject.toml | 4 + .../pip/existing_project/requirements.txt | 19 +++++ .../pip_tools/existing_project/pyproject.toml | 4 + .../existing_project/requirements.in | 1 + .../existing_project/requirements.txt | 12 +++ .../fixtures/pipenv/existing_project/Pipfile | 16 ++++ .../pipenv/existing_project/pyproject.toml | 4 + .../poetry/existing_project/pyproject.toml | 19 +++++ tests/pip.rs | 84 +++++++++++++++++++ tests/pip_tools.rs | 46 ++++++++++ tests/pipenv.rs | 71 ++++++++++++++++ tests/poetry.rs | 63 ++++++++++++++ 20 files changed, 432 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/pip/existing_project/pyproject.toml create mode 100644 tests/fixtures/pip/existing_project/requirements.txt create mode 100644 tests/fixtures/pip_tools/existing_project/pyproject.toml create mode 100644 tests/fixtures/pip_tools/existing_project/requirements.in create mode 100644 tests/fixtures/pip_tools/existing_project/requirements.txt create mode 100644 tests/fixtures/pipenv/existing_project/Pipfile create mode 100644 tests/fixtures/pipenv/existing_project/pyproject.toml create mode 100644 tests/fixtures/poetry/existing_project/pyproject.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index 42c14b5..97bb10e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## Unreleased + +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: + +```bash +migrate-to-uv --replace-project-section +``` + +### Features + +* Preserve existing data in `[project]` section of `pyproject.toml` when migrating ([#84](https://github.com/mkniewallner/migrate-to-uv/pull/84)) + ## 0.5.0 - 2025-01-18 ### Features diff --git a/docs/usage-and-configuration.md b/docs/usage-and-configuration.md index 6c6f821..285ac39 100644 --- a/docs/usage-and-configuration.md +++ b/docs/usage-and-configuration.md @@ -66,6 +66,17 @@ constraints. migrate-to-uv --ignore-locked-versions ``` +#### `--replace-project-section` + +By default, existing data in `[project]` section of `pyproject.toml` is preserved when migrating. This flag allows +completely replacing existing content. + +**Example**: + +```bash +migrate-to-uv --replace-project-section +``` + #### `--package-manager` By default, `migrate-to-uv` tries to auto-detect the package manager based on the files (and their content) used by the diff --git a/src/cli.rs b/src/cli.rs index 0e11085..e63e584 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -41,6 +41,11 @@ struct Cli { help = "Ignore current locked versions of dependencies when generating `uv.lock`" )] ignore_locked_versions: bool, + #[arg( + long, + help = "Replace existing data in `[project]` section of `pyproject.toml` instead of keeping existing fields" + )] + replace_project_section: bool, #[arg( long, help = "Enforce a specific package manager instead of auto-detecting it" @@ -72,6 +77,7 @@ pub fn cli() { dry_run: cli.dry_run, skip_lock: cli.skip_lock, ignore_locked_versions: cli.ignore_locked_versions, + replace_project_section: cli.replace_project_section, keep_old_metadata: cli.keep_current_data, dependency_groups_strategy: cli.dependency_groups_strategy, }; diff --git a/src/converters/mod.rs b/src/converters/mod.rs index f08708d..718f4bd 100644 --- a/src/converters/mod.rs +++ b/src/converters/mod.rs @@ -1,4 +1,5 @@ use crate::converters::pyproject_updater::PyprojectUpdater; +use crate::schema::pep_621::Project; use crate::schema::pyproject::DependencyGroupSpecification; use indexmap::IndexMap; use log::{error, info, warn}; @@ -30,6 +31,7 @@ pub struct ConverterOptions { pub dry_run: bool, pub skip_lock: bool, pub ignore_locked_versions: bool, + pub replace_project_section: bool, pub keep_old_metadata: bool, pub dependency_groups_strategy: DependencyGroupsStrategy, } @@ -74,6 +76,40 @@ pub trait Converter: Debug { /// Build `pyproject.toml` for uv package manager based on current package manager data. fn build_uv_pyproject(&self) -> String; + /// Build PEP 621 `[project]` section, keeping existing fields if the section is already + /// defined, unless user has chosen to replace existing section. + fn build_project(&self, current_project: Option, project: Project) -> Project { + if self.replace_project_section() { + return project; + } + + let Some(current_project) = current_project else { + return project; + }; + + Project { + name: current_project.name.or(project.name), + version: current_project.version.or(project.version), + description: current_project.description.or(project.description), + authors: current_project.authors.or(project.authors), + requires_python: current_project.requires_python.or(project.requires_python), + readme: current_project.readme.or(project.readme), + license: current_project.license.or(project.license), + maintainers: current_project.maintainers.or(project.maintainers), + keywords: current_project.keywords.or(project.keywords), + classifiers: current_project.classifiers.or(project.classifiers), + dependencies: current_project.dependencies.or(project.dependencies), + optional_dependencies: current_project + .optional_dependencies + .or(project.optional_dependencies), + urls: current_project.urls.or(project.urls), + scripts: current_project.scripts.or(project.scripts), + gui_scripts: current_project.gui_scripts.or(project.gui_scripts), + entry_points: current_project.entry_points.or(project.entry_points), + remaining_fields: current_project.remaining_fields, + } + } + /// Name of the current package manager. fn get_package_manager_name(&self) -> String; @@ -97,6 +133,12 @@ pub trait Converter: Debug { self.get_converter_options().skip_lock } + /// Whether to replace existing `[project]` section of `pyproject.toml`, or to keep existing + /// fields. + fn replace_project_section(&self) -> bool { + self.get_converter_options().replace_project_section + } + /// Whether to keep current package manager data at the end of the migration. fn keep_old_metadata(&self) -> bool { self.get_converter_options().keep_old_metadata diff --git a/src/converters/pip/mod.rs b/src/converters/pip/mod.rs index 7e47558..c6a9f01 100644 --- a/src/converters/pip/mod.rs +++ b/src/converters/pip/mod.rs @@ -1,11 +1,10 @@ mod dependencies; -use crate::converters::pip::dependencies::get; use crate::converters::pyproject_updater::PyprojectUpdater; use crate::converters::Converter; use crate::converters::ConverterOptions; use crate::schema::pep_621::Project; -use crate::schema::pyproject::DependencyGroupSpecification; +use crate::schema::pyproject::{DependencyGroupSpecification, PyProject}; use crate::schema::uv::Uv; use crate::toml::PyprojectPrettyFormatter; use indexmap::IndexMap; @@ -26,6 +25,10 @@ pub struct Pip { impl Converter for Pip { fn build_uv_pyproject(&self) -> String { + let pyproject_toml_content = + fs::read_to_string(self.get_project_path().join("pyproject.toml")).unwrap_or_default(); + let pyproject: PyProject = toml::from_str(pyproject_toml_content.as_str()).unwrap(); + let dev_dependencies = dependencies::get( &self.get_project_path(), self.dev_requirements_files.clone(), @@ -73,7 +76,7 @@ impl Converter for Pip { pyproject: &mut updated_pyproject, }; - pyproject_updater.insert_pep_621(&project); + pyproject_updater.insert_pep_621(&self.build_project(pyproject.project, project)); pyproject_updater.insert_dependency_groups(dependency_groups.as_ref()); pyproject_updater.insert_uv(&uv); @@ -126,7 +129,7 @@ impl Converter for Pip { return None; } - if let Some(dependencies) = get( + if let Some(dependencies) = dependencies::get( self.get_project_path().as_path(), self.requirements_files .clone() diff --git a/src/converters/pipenv/mod.rs b/src/converters/pipenv/mod.rs index 9438707..2f136e7 100644 --- a/src/converters/pipenv/mod.rs +++ b/src/converters/pipenv/mod.rs @@ -7,6 +7,7 @@ use crate::converters::Converter; use crate::converters::ConverterOptions; use crate::schema::pep_621::Project; use crate::schema::pipenv::{PipenvLock, Pipfile}; +use crate::schema::pyproject::PyProject; use crate::schema::uv::{SourceContainer, Uv}; use crate::toml::PyprojectPrettyFormatter; use indexmap::IndexMap; @@ -26,6 +27,10 @@ pub struct Pipenv { impl Converter for Pipenv { fn build_uv_pyproject(&self) -> String { + let pyproject_toml_content = + fs::read_to_string(self.get_project_path().join("pyproject.toml")).unwrap_or_default(); + let pyproject: PyProject = toml::from_str(pyproject_toml_content.as_str()).unwrap(); + let pipfile_content = fs::read_to_string(self.get_project_path().join("Pipfile")).unwrap(); let pipfile: Pipfile = toml::from_str(pipfile_content.as_str()).unwrap(); @@ -66,7 +71,7 @@ impl Converter for Pipenv { pyproject: &mut updated_pyproject, }; - pyproject_updater.insert_pep_621(&project); + pyproject_updater.insert_pep_621(&self.build_project(pyproject.project, project)); pyproject_updater.insert_dependency_groups(dependency_groups.as_ref()); pyproject_updater.insert_uv(&uv); @@ -156,6 +161,7 @@ mod tests { dry_run: true, skip_lock: true, ignore_locked_versions: true, + replace_project_section: false, keep_old_metadata: false, dependency_groups_strategy: DependencyGroupsStrategy::SetDefaultGroups, }, @@ -188,6 +194,7 @@ mod tests { dry_run: true, skip_lock: true, ignore_locked_versions: true, + replace_project_section: false, keep_old_metadata: false, dependency_groups_strategy: DependencyGroupsStrategy::SetDefaultGroups, }, diff --git a/src/converters/poetry/mod.rs b/src/converters/poetry/mod.rs index 9cb71a7..82daee6 100644 --- a/src/converters/poetry/mod.rs +++ b/src/converters/poetry/mod.rs @@ -112,7 +112,7 @@ impl Converter for Poetry { pyproject_updater.insert_build_system( build_backend::get_new_build_system(pyproject.build_system).as_ref(), ); - pyproject_updater.insert_pep_621(&project); + pyproject_updater.insert_pep_621(&self.build_project(pyproject.project, project)); pyproject_updater.insert_dependency_groups(dependency_groups.as_ref()); pyproject_updater.insert_uv(&uv); pyproject_updater.insert_hatch(hatch.as_ref()); @@ -216,6 +216,7 @@ mod tests { dry_run: true, skip_lock: true, ignore_locked_versions: true, + replace_project_section: false, keep_old_metadata: false, dependency_groups_strategy: DependencyGroupsStrategy::SetDefaultGroups, }, diff --git a/src/detector.rs b/src/detector.rs index cd34fb7..e21c950 100644 --- a/src/detector.rs +++ b/src/detector.rs @@ -251,6 +251,7 @@ mod tests { dry_run: true, skip_lock: true, ignore_locked_versions: false, + replace_project_section: false, keep_old_metadata: false, dependency_groups_strategy: DependencyGroupsStrategy::SetDefaultGroups, } diff --git a/tests/fixtures/pip/existing_project/pyproject.toml b/tests/fixtures/pip/existing_project/pyproject.toml new file mode 100644 index 0000000..4f3fb8e --- /dev/null +++ b/tests/fixtures/pip/existing_project/pyproject.toml @@ -0,0 +1,4 @@ +[project] +name = "foobar" +version = "1.0.0" +requires-python = ">=3.13" diff --git a/tests/fixtures/pip/existing_project/requirements.txt b/tests/fixtures/pip/existing_project/requirements.txt new file mode 100644 index 0000000..0cfa754 --- /dev/null +++ b/tests/fixtures/pip/existing_project/requirements.txt @@ -0,0 +1,19 @@ +# A comment +anyio==4.7.0 +arrow==1.3.0 +certifi==2024.12.14 +click==8.1.8 +h11==0.14.0 +httpcore==1.0.7 +httpx==0.28.1 +idna==3.10 +markdown-it-py==3.0.0 +mdurl==0.1.2 +pygments==2.18.0 +python-dateutil==2.9.0.post0 +rich==13.9.4 +six==1.17.0 +sniffio==1.3.1 +types-python-dateutil==2.9.0.20241206 +uvicorn @ git+https://github.com/encode/uvicorn +zstandard==0.23.0 diff --git a/tests/fixtures/pip_tools/existing_project/pyproject.toml b/tests/fixtures/pip_tools/existing_project/pyproject.toml new file mode 100644 index 0000000..4f3fb8e --- /dev/null +++ b/tests/fixtures/pip_tools/existing_project/pyproject.toml @@ -0,0 +1,4 @@ +[project] +name = "foobar" +version = "1.0.0" +requires-python = ">=3.13" diff --git a/tests/fixtures/pip_tools/existing_project/requirements.in b/tests/fixtures/pip_tools/existing_project/requirements.in new file mode 100644 index 0000000..4070f5b --- /dev/null +++ b/tests/fixtures/pip_tools/existing_project/requirements.in @@ -0,0 +1 @@ +arrow>=1.2.3 diff --git a/tests/fixtures/pip_tools/existing_project/requirements.txt b/tests/fixtures/pip_tools/existing_project/requirements.txt new file mode 100644 index 0000000..d520038 --- /dev/null +++ b/tests/fixtures/pip_tools/existing_project/requirements.txt @@ -0,0 +1,12 @@ +# +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: +# +# pip-compile requirements.in +# +arrow==1.2.3 + # via -r requirements.in +python-dateutil==2.7.0 + # via arrow +six==1.15.0 + # via python-dateutil diff --git a/tests/fixtures/pipenv/existing_project/Pipfile b/tests/fixtures/pipenv/existing_project/Pipfile new file mode 100644 index 0000000..ef0bb15 --- /dev/null +++ b/tests/fixtures/pipenv/existing_project/Pipfile @@ -0,0 +1,16 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +arrow = ">=1.2.3" + +[dev-packages] +mypy = ">=1.13.0" + +[test] +factory-boy = ">=3.2.1" + +[requires] +python_version = "3.13" diff --git a/tests/fixtures/pipenv/existing_project/pyproject.toml b/tests/fixtures/pipenv/existing_project/pyproject.toml new file mode 100644 index 0000000..4f3fb8e --- /dev/null +++ b/tests/fixtures/pipenv/existing_project/pyproject.toml @@ -0,0 +1,4 @@ +[project] +name = "foobar" +version = "1.0.0" +requires-python = ">=3.13" diff --git a/tests/fixtures/poetry/existing_project/pyproject.toml b/tests/fixtures/poetry/existing_project/pyproject.toml new file mode 100644 index 0000000..4966a84 --- /dev/null +++ b/tests/fixtures/poetry/existing_project/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "foobar" +version = "1.0.0" +requires-python = ">=3.13" + +[tool.poetry] +name = "foo" +version = "0.0.1" +description = "A description" + +[tool.poetry.dependencies] +python = "^3.11" +arrow = "^1.2.3" + +[tool.poetry.group.dev.dependencies] +factory-boy = "^3.2.1" + +[tool.poetry.group.typing.dependencies] +mypy = "^1.13.0" diff --git a/tests/pip.rs b/tests/pip.rs index f035b1c..cbf0a1d 100644 --- a/tests/pip.rs +++ b/tests/pip.rs @@ -343,3 +343,87 @@ fn test_dry_run() { // Assert that `uv.lock` file was not generated. assert!(!project_path.join("uv.lock").exists()); } + +#[test] +fn test_preserves_existing_project() { + let project_path = Path::new(FIXTURES_PATH).join("existing_project"); + + assert_cmd_snapshot!(cli().arg(&project_path).arg("--dry-run"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Migrated pyproject.toml: + [project] + name = "foobar" + version = "1.0.0" + requires-python = ">=3.13" + dependencies = [ + "anyio==4.7.0", + "arrow==1.3.0", + "certifi==2024.12.14", + "click==8.1.8", + "h11==0.14.0", + "httpcore==1.0.7", + "httpx==0.28.1", + "idna==3.10", + "markdown-it-py==3.0.0", + "mdurl==0.1.2", + "pygments==2.18.0", + "python-dateutil==2.9.0.post0", + "rich==13.9.4", + "six==1.17.0", + "sniffio==1.3.1", + "types-python-dateutil==2.9.0.20241206", + "uvicorn @ git+https://github.com/encode/uvicorn", + "zstandard==0.23.0", + ] + + [tool.uv] + package = false + "###); +} + +#[test] +fn test_replaces_existing_project() { + let project_path = Path::new(FIXTURES_PATH).join("existing_project"); + + assert_cmd_snapshot!(cli() + .arg(&project_path) + .arg("--dry-run") + .arg("--replace-project-section"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Migrated pyproject.toml: + [project] + name = "" + version = "0.0.1" + dependencies = [ + "anyio==4.7.0", + "arrow==1.3.0", + "certifi==2024.12.14", + "click==8.1.8", + "h11==0.14.0", + "httpcore==1.0.7", + "httpx==0.28.1", + "idna==3.10", + "markdown-it-py==3.0.0", + "mdurl==0.1.2", + "pygments==2.18.0", + "python-dateutil==2.9.0.post0", + "rich==13.9.4", + "six==1.17.0", + "sniffio==1.3.1", + "types-python-dateutil==2.9.0.20241206", + "uvicorn @ git+https://github.com/encode/uvicorn", + "zstandard==0.23.0", + ] + + [tool.uv] + package = false + "###); +} diff --git a/tests/pip_tools.rs b/tests/pip_tools.rs index 9e4a8ad..cb1c124 100644 --- a/tests/pip_tools.rs +++ b/tests/pip_tools.rs @@ -445,3 +445,49 @@ fn test_dry_run() { // Assert that `uv.lock` file was not generated. assert!(!project_path.join("uv.lock").exists()); } + +#[test] +fn test_preserves_existing_project() { + let project_path = Path::new(FIXTURES_PATH).join("existing_project"); + + assert_cmd_snapshot!(cli().arg(&project_path).arg("--dry-run"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Migrated pyproject.toml: + [project] + name = "foobar" + version = "1.0.0" + requires-python = ">=3.13" + dependencies = ["arrow>=1.2.3"] + + [tool.uv] + package = false + "###); +} + +#[test] +fn test_replaces_existing_project() { + let project_path = Path::new(FIXTURES_PATH).join("existing_project"); + + assert_cmd_snapshot!(cli() + .arg(&project_path) + .arg("--dry-run") + .arg("--replace-project-section"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Migrated pyproject.toml: + [project] + name = "" + version = "0.0.1" + dependencies = ["arrow>=1.2.3"] + + [tool.uv] + package = false + "###); +} diff --git a/tests/pipenv.rs b/tests/pipenv.rs index a70366a..109fed5 100644 --- a/tests/pipenv.rs +++ b/tests/pipenv.rs @@ -633,3 +633,74 @@ fn test_dry_run_minimal() { // Assert that `uv.lock` file was not generated. assert!(!project_path.join("uv.lock").exists()); } + +#[test] +fn test_preserves_existing_project() { + let project_path = Path::new(FIXTURES_PATH).join("existing_project"); + + assert_cmd_snapshot!(cli().arg(&project_path).arg("--dry-run"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Migrated pyproject.toml: + [project] + name = "foobar" + version = "1.0.0" + requires-python = ">=3.13" + dependencies = ["arrow>=1.2.3"] + + [dependency-groups] + dev = ["mypy>=1.13.0"] + test = ["factory-boy>=3.2.1"] + + [tool.uv] + package = false + default-groups = [ + "dev", + "test", + ] + + [[tool.uv.index]] + name = "pypi" + url = "https://pypi.org/simple" + "###); +} + +#[test] +fn test_replaces_existing_project() { + let project_path = Path::new(FIXTURES_PATH).join("existing_project"); + + assert_cmd_snapshot!(cli() + .arg(&project_path) + .arg("--dry-run") + .arg("--replace-project-section"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Migrated pyproject.toml: + [project] + name = "" + version = "0.0.1" + requires-python = "~=3.13" + dependencies = ["arrow>=1.2.3"] + + [dependency-groups] + dev = ["mypy>=1.13.0"] + test = ["factory-boy>=3.2.1"] + + [tool.uv] + package = false + default-groups = [ + "dev", + "test", + ] + + [[tool.uv.index]] + name = "pypi" + url = "https://pypi.org/simple" + "###); +} diff --git a/tests/poetry.rs b/tests/poetry.rs index a17555e..5604064 100644 --- a/tests/poetry.rs +++ b/tests/poetry.rs @@ -804,3 +804,66 @@ fn test_dry_run_minimal() { // Assert that `uv.lock` file was not generated. assert!(!project_path.join("uv.lock").exists()); } + +#[test] +fn test_preserves_existing_project() { + let project_path = Path::new(FIXTURES_PATH).join("existing_project"); + + assert_cmd_snapshot!(cli().arg(&project_path).arg("--dry-run"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Migrated pyproject.toml: + [project] + name = "foobar" + version = "1.0.0" + description = "A description" + requires-python = ">=3.13" + dependencies = ["arrow>=1.2.3,<2"] + + [dependency-groups] + dev = ["factory-boy>=3.2.1,<4"] + typing = ["mypy>=1.13.0,<2"] + + [tool.uv] + default-groups = [ + "dev", + "typing", + ] + "###); +} + +#[test] +fn test_replaces_existing_project() { + let project_path = Path::new(FIXTURES_PATH).join("existing_project"); + + assert_cmd_snapshot!(cli() + .arg(&project_path) + .arg("--dry-run") + .arg("--replace-project-section"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Migrated pyproject.toml: + [project] + name = "foo" + version = "0.0.1" + description = "A description" + requires-python = "~=3.11" + dependencies = ["arrow>=1.2.3,<2"] + + [dependency-groups] + dev = ["factory-boy>=3.2.1,<4"] + typing = ["mypy>=1.13.0,<2"] + + [tool.uv] + default-groups = [ + "dev", + "typing", + ] + "###); +}