Skip to content

Commit

Permalink
feat(poetry): migrate packages (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
mkniewallner authored Jan 5, 2025
1 parent c4c3dcb commit 8f3ea12
Show file tree
Hide file tree
Showing 15 changed files with 673 additions and 89 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

* Support migrating projects using `pip` and `pip-tools` ([#24](https://github.com/mkniewallner/migrate-to-uv/pull/24))
* [poetry] Migrate data from `packages`, `include` and `exclude` to Hatch build backend ([#16](https://github.com/mkniewallner/migrate-to-uv/pull/16))

## 0.1.2 - 2025-01-02

Expand Down
20 changes: 16 additions & 4 deletions docs/supported-package-managers.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ be used, use [`--package-manager`](usage-and-configuration.md#-package-manager).

## Poetry

Most [Poetry](https://python-poetry.org/) metadata is converted to uv when performing the migration:
All existing [Poetry](https://python-poetry.org/) metadata should be converted to uv when performing the migration:

- [Project metadata](https://python-poetry.org/docs/pyproject/) (`name`, `version`, `authors`, ...)
- [Dependencies and dependency groups](https://python-poetry.org/docs/pyproject/#dependencies-and-dependency-groups)
Expand All @@ -16,6 +16,7 @@ Most [Poetry](https://python-poetry.org/) metadata is converted to uv when perfo
- [Dependency markers](https://python-poetry.org/docs/dependency-specification#using-environment-markers) (including
[`python`](https://python-poetry.org/docs/dependency-specification/#python-restricted-dependencies) and `platform`)
- [Multiple constraints dependencies](https://python-poetry.org/docs/dependency-specification#multiple-constraints-dependencies)
- Package distribution metadata ([`packages`](https://python-poetry.org/docs/pyproject/#packages), [`include` and `exclude`](https://python-poetry.org/docs/pyproject/#include-and-exclude))
- [Supported Python versions](https://python-poetry.org/docs/basic-usage/#setting-a-python-version)
- [Scripts](https://python-poetry.org/docs/pyproject/#scripts) and
[plugins](https://python-poetry.org/docs/pyproject/#plugins) (also known as entry points)
Expand All @@ -25,11 +26,22 @@ equivalent [PEP 440](https://peps.python.org/pep-0440/) format used by uv, even
specification (e.g., [caret](https://python-poetry.org/docs/dependency-specification/#caret-requirements) (`^`)
and [tilde](https://python-poetry.org/docs/dependency-specification/#tilde-requirements) (`~`)).

### Missing features
### Build backend

The following features are not yet supported when migrating:
As uv does not yet have a stable build backend (see [astral-sh/uv#8779](https://github.com/astral-sh/uv/issues/8779) for more details), when
performing the migration for libraries, `migrate-to-uv` sets [Hatch](https://hatch.pypa.io/latest/) as a build
backend, migrating:

- Package distribution metadata ([`packages`](https://python-poetry.org/docs/pyproject/#packages), [`include` and `exclude`](https://python-poetry.org/docs/pyproject/#include-and-exclude))
- Poetry [`packages`](https://python-poetry.org/docs/pyproject/#packages) and [`include`](https://python-poetry.org/docs/pyproject/#include-and-exclude) to Hatch [`include`](https://hatch.pypa.io/latest/config/build/#patterns)
- Poetry [`exclude`](https://python-poetry.org/docs/pyproject/#include-and-exclude) to Hatch [`exclude`](https://hatch.pypa.io/latest/config/build/#patterns)

!!! note

Path rewriting, defined with `to` in `packages` for Poetry, is also migrated to Hatch by defining
[sources](https://hatch.pypa.io/latest/config/build/#rewriting-paths) in wheel target.


Once uv build backend is out of preview and considered stable, it will be used for the migration.

## Pipenv

Expand Down
262 changes: 243 additions & 19 deletions src/converters/poetry/build_backend.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,261 @@
use crate::schema::poetry::{IncludeExclude, Package};
use crate::schema::hatch::{Build, BuildTarget, Hatch};
use crate::schema::poetry::{Format, Include, Package};
use crate::schema::pyproject::BuildSystem;
use log::warn;
use owo_colors::OwoColorize;
use crate::schema::utils::SingleOrVec;
use indexmap::IndexMap;
use std::path::{Path, PathBuf, MAIN_SEPARATOR};

type HatchTargetsIncludeAndSource = (
Option<Vec<String>>,
Option<Vec<String>>,
Option<IndexMap<String, String>>,
);

pub fn get_new_build_system(build_system: Option<BuildSystem>) -> Option<BuildSystem> {
if build_system?.build_backend? == "poetry.core.masonry.api" {
return Some(BuildSystem {
requires: vec!["uv>=0.5,<0.6".to_string()],
build_backend: Some("uv".to_string()),
requires: vec!["hatchling".to_string()],
build_backend: Some("hatchling.build".to_string()),
});
}
None
}

/// Warns that migration of package-related keys is not yet supported, if we find them.
pub fn warn_unsupported_package_keys(
/// Construct hatch package metadata (<https://hatch.pypa.io/latest/config/build/>) from Poetry
/// `packages` (<https://python-poetry.org/docs/pyproject/#packages>) and `include`/`exclude`
/// (<https://python-poetry.org/docs/pyproject/#include-and-exclude>).
///
/// Poetry `packages` and `include` are converted to hatch `include`.
///
/// If a pattern in `packages` uses `to`, an entry is populated in `sources` under hatch `wheel`
/// target to rewrite the path the same way as Poetry does in wheels. Note that although Poetry's
/// documentation does not specify it, `to` only rewrites paths in wheels, and not sdist, so we only
/// apply path rewriting in `wheel` target.
///
/// Poetry `exclude` is converted as is to hatch `exclude`.
///
pub fn get_hatch(
packages: Option<&Vec<Package>>,
include: Option<&Vec<Include>>,
exclude: Option<&Vec<String>>,
) -> Option<Hatch> {
let mut targets = IndexMap::new();
let (sdist_include, wheel_include, wheel_sources) = get_hatch_include(packages, include);

let sdist_target = BuildTarget {
include: sdist_include,
exclude: exclude.cloned(),
sources: None,
};
let wheel_target = BuildTarget {
include: wheel_include,
exclude: exclude.cloned(),
sources: wheel_sources,
};

if sdist_target != BuildTarget::default() {
targets.insert("sdist".to_string(), sdist_target);
}
if wheel_target != BuildTarget::default() {
targets.insert("wheel".to_string(), wheel_target);
}

if targets.is_empty() {
return None;
}

Some(Hatch {
build: Some(Build {
targets: Some(targets),
}),
})
}

/// Inclusion behavior: <https://hatch.pypa.io/latest/config/build/#patterns>
/// Path rewriting behavior: <https://hatch.pypa.io/latest/config/build/#rewriting-paths>
fn get_hatch_include(
packages: Option<&Vec<Package>>,
include: Option<&Vec<IncludeExclude>>,
exclude: Option<&Vec<IncludeExclude>>,
) {
let mut detected_package_keys = Vec::new();
include: Option<&Vec<Include>>,
) -> HatchTargetsIncludeAndSource {
let mut sdist_include = Vec::new();
let mut wheel_include = Vec::new();
let mut wheel_sources = IndexMap::new();

// https://python-poetry.org/docs/pyproject/#packages
if let Some(packages) = packages {
for Package {
include,
format,
from,
to,
} in packages
{
let include_with_from = PathBuf::from(from.as_ref().map_or("", |from| from))
.join(include)
.display()
.to_string()
// Ensure that separator remains "/" (Windows uses "\").
.replace(MAIN_SEPARATOR, "/");

match format {
None => {
sdist_include.push(include_with_from.clone());
wheel_include.push(include_with_from.clone());

if let Some((from, to)) = get_hatch_source(
include.clone(),
include_with_from.clone(),
to.as_ref(),
from.as_ref(),
) {
wheel_sources.insert(from, to);
}
}
Some(SingleOrVec::Single(Format::Sdist)) => {
sdist_include.push(include_with_from.clone());
}
Some(SingleOrVec::Single(Format::Wheel)) => {
wheel_include.push(include_with_from.clone());

if packages.is_some() {
detected_package_keys.push("packages");
if let Some((from, to)) = get_hatch_source(
include.clone(),
include_with_from.clone(),
to.as_ref(),
from.as_ref(),
) {
wheel_sources.insert(from, to);
}
}
Some(SingleOrVec::Vec(vec)) => {
if vec.contains(&Format::Sdist) || vec.is_empty() {
sdist_include.push(include_with_from.clone());
}
if vec.contains(&Format::Wheel) || vec.is_empty() {
wheel_include.push(include_with_from.clone());

if let Some((from, to)) = get_hatch_source(
include.clone(),
include_with_from,
to.as_ref(),
from.as_ref(),
) {
wheel_sources.insert(from, to);
}
}
}
}
}
}

// https://python-poetry.org/docs/pyproject/#include-and-exclude
if let Some(include) = include {
for inc in include {
match inc {
Include::String(path) | Include::Map { path, format: None } => {
sdist_include.push(path.to_string());
wheel_include.push(path.to_string());
}
Include::Map {
path,
format: Some(SingleOrVec::Vec(format)),
} => match format[..] {
[] | [Format::Sdist, Format::Wheel] => {
sdist_include.push(path.to_string());
wheel_include.push(path.to_string());
}
[Format::Sdist] => sdist_include.push(path.to_string()),
[Format::Wheel] => wheel_include.push(path.to_string()),
_ => (),
},
Include::Map {
path,
format: Some(SingleOrVec::Single(Format::Sdist)),
} => sdist_include.push(path.to_string()),
Include::Map {
path,
format: Some(SingleOrVec::Single(Format::Wheel)),
} => wheel_include.push(path.to_string()),
}
}
}

(
if sdist_include.is_empty() {
None
} else {
Some(sdist_include)
},
if wheel_include.is_empty() {
None
} else {
Some(wheel_include)
},
if wheel_sources.is_empty() {
None
} else {
Some(wheel_sources)
},
)
}

/// Get hatch source, to rewrite path from a directory to another directory in the built artifact.
/// <https://hatch.pypa.io/latest/config/build/#rewriting-paths>
fn get_hatch_source(
include: String,
include_with_from: String,
to: Option<&String>,
from: Option<&String>,
) -> Option<(String, String)> {
if let Some(to) = to {
return if include.contains('*') {
// Hatch path rewrite behaves differently to Poetry, as rewriting is only possible on
// static paths, so we build the longest path until we reach a glob for both the initial
// and the path to rewrite to, to only rewrite the static part for both.
let from_without_glob = extract_parent_path_from_glob(&include_with_from)?;
let to_without_glob = extract_parent_path_from_glob(&include)?;

Some((
from_without_glob,
Path::new(to)
.join(to_without_glob)
.display()
.to_string()
// Ensure that separator remains "/" (Windows uses "\").
.replace(MAIN_SEPARATOR, "/"),
))
} else {
Some((
include_with_from,
Path::new(to)
.join(include)
.display()
.to_string()
// Ensure that separator remains "/" (Windows uses "\").
.replace(MAIN_SEPARATOR, "/"),
))
};
}
if include.is_some() {
detected_package_keys.push("include");

if from.is_some() {
return Some((include_with_from, include));
}
if exclude.is_some() {
detected_package_keys.push("exclude");

None
}

/// Extract the longest path part from a path until a glob is found.
fn extract_parent_path_from_glob(s: &str) -> Option<String> {
let mut parents = Vec::new();

for part in s.split('/') {
if part.contains('*') {
break;
}
parents.push(part);
}

if !detected_package_keys.is_empty() {
warn!("Migration of package specification is not yet supported, so the following keys under {} were not migrated: {}.", "[tool.poetry]".bold(), detected_package_keys.join(", ").bold());
if parents.is_empty() {
return None;
}
Some(parents.join("/"))
}
5 changes: 3 additions & 2 deletions src/converters/poetry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod project;
mod sources;
pub mod version;

use crate::converters::poetry::build_backend::warn_unsupported_package_keys;
use crate::converters::poetry::build_backend::get_hatch;
use crate::converters::pyproject_updater::PyprojectUpdater;
use crate::converters::Converter;
use crate::converters::DependencyGroupsStrategy;
Expand Down Expand Up @@ -145,7 +145,7 @@ fn perform_migration(
default_groups: uv_default_groups,
};

warn_unsupported_package_keys(
let hatch = get_hatch(
poetry.packages.as_ref(),
poetry.include.as_ref(),
poetry.exclude.as_ref(),
Expand All @@ -161,6 +161,7 @@ fn perform_migration(
pyproject_updater.insert_pep_621(&project);
pyproject_updater.insert_dependency_groups(dependency_groups.as_ref());
pyproject_updater.insert_uv(&uv);
pyproject_updater.insert_hatch(hatch.as_ref());

if !keep_old_metadata {
remove_pyproject_poetry_section(&mut updated_pyproject);
Expand Down
Loading

0 comments on commit 8f3ea12

Please sign in to comment.