poetry2nix/vendor/pyproject.nix/lib/poetry.nix
2023-10-26 17:02:10 +13:00

258 lines
7.9 KiB
Nix

{ lib
, pep440
, pep508
, pep518
, ...
}:
lib.fix (self:
let
inherit (builtins) match elemAt foldl' typeOf attrNames head tail mapAttrs length filter split;
inherit (lib) optionalAttrs flatten;
inherit (import ./util.nix { inherit lib; }) splitComma;
# Translate author from a string like "Name <email>" to a structured set as defined by PEP-621.
translateAuthor = a:
let
mAt = elemAt (match "^(.+) <(.+)>$" a);
in
{ name = mAt 0; email = mAt 1; };
# Normalize dependecy from poetry dependencies table from (string || set) -> set
normalizeDep = name: dep: (
let
type = typeOf dep;
in
if type == "string" then {
inherit name;
version = dep;
}
else if type == "set" then dep // { inherit name; }
else throw "Unexpected type: ${type}"
);
# Rewrite the right hand side version for caret comparisons according to the rules laid out in
# https://python-poetry.org/docs/dependency-specification/#caret-requirements
rewriteCaretRhs = release:
let
state = foldl'
(state: v:
let
nonzero = state.nonzero || v != 0;
in
state // {
release = state.release ++ [
(
if nonzero && !state.nonzero then (v + 1)
else if nonzero then 0
else v
)
];
inherit nonzero;
})
{
release = [ ];
nonzero = false;
}
release;
in
if !state.nonzero
then ([ (head state.release + 1) ] ++ tail state.release)
else state.release;
# Poetry dependency tables are of mixed types:
# [tool.poetry.dependencies]
# python = "^3.8"
# cachecontrol = { version = "^0.13.0", extras = ["filecache"] }
# foo = [
# {version = "<=1.9", python = ">=3.6,<3.8"},
# {version = "^2.0", python = ">=3.8"}
# ]
#
# These are all valid. Normalize the input to a list of:
# [
# { name = "python"; version = "^3.8"; }
# { name = "cachecontrol"; version = "^0.13.0"; extras = ["filecache"]; }
# { name = "foo"; version = "<=1.9"; python = ">=3.6,<3.8"; }
# { name = "foo"; version = "^2.0"; python = ">=3.8"; }
# ]
normalizeDependendenciesToList = deps: foldl'
(acc: name: acc ++ (
let
dep = deps.${name};
in
if typeOf dep == "list" then map (normalizeDep name) dep
else [ (normalizeDep name dep) ]
)) [ ]
(attrNames deps);
# Normalized version of parseVersionCond'
parseVersionConds = s: flatten (map self.parseVersionCond (splitComma s));
dummyMarker = {
type = "bool";
value = true;
};
# Analogous to pep508.parseString
parseDependency = dep:
let
# Poetry has Python as a separate field in the structured dependency object.
# This is non-standard. Rewrite these expressions as a nested set of logical ANDs that
# looks like regular parsed markers as if they were standard PEP-508, just written in a bit of a funky
# nested way that no human would do.
markers = foldl'
(rhs: pyCond: {
type = "boolOp";
op = "and";
lhs = {
type = "compare";
inherit (pyCond) op;
lhs = {
type = "variable";
value = "python_version";
};
rhs = {
type = "version";
value = pyCond.version;
};
};
inherit rhs;
})
(
# Encode no markers as a marker that always evaluates to true to simplify fold logi above.
if dep ? markers then pep508.parseMarkers dep.markers else dummyMarker
)
(if dep ? python then parseVersionConds dep.python else [ ]);
in
{
inherit (dep) name;
conditions = parseVersionConds dep.version;
extras = dep.extras or [ ];
url = dep.url or null;
markers = if markers == dummyMarker then null else markers;
};
in
{
/*
Translate a Pyproject.toml from Poetry to PEP-621 project metadata.
This function transposes a PEP-621 project table on top of an existing Pyproject.toml populated with data from `tool.poetry`.
Notably does not translate dependencies/optional-dependencies.
For parsing dependencies from Poetry see `lib.poetry.parseDependencies`.
Type: translatePoetryProject :: AttrSet -> lambda
Example:
# translatePoetryProject (lib.importTOML ./pyproject.toml)
{ } # TOML contents, structure omitted. See PEP-621 for more information on data members.
*/
translatePoetryProject = pyproject: assert !(pyproject ? project); let
inherit (pyproject.tool) poetry;
in
pyproject // {
project = {
inherit (poetry) name version description;
authors = map translateAuthor poetry.authors;
urls = optionalAttrs (poetry ? homepage)
{
Homepage = poetry.homepage;
} // optionalAttrs (poetry ? repository) {
Repository = poetry.repository;
} // optionalAttrs (poetry ? documentation) {
Documentation = poetry.documentation;
};
} // optionalAttrs (poetry ? license) {
license.text = poetry.license;
} // optionalAttrs (poetry ? maintainers) {
maintainers = map translateAuthor poetry.maintainers;
} // optionalAttrs (poetry ? readme) {
inherit (poetry) readme;
} // optionalAttrs (poetry ? keywords) {
inherit (poetry) keywords;
} // optionalAttrs (poetry ? classifiers) {
inherit (poetry) classifiers;
};
};
/* Parse dependencies from pyproject.toml (Poetry edition).
This function is analogous to `lib.pep621.parseDependencies`.
Type: parseDependencies :: AttrSet -> AttrSet
Example:
# parseDependencies {
#
# pyproject = (lib.importTOML ./pyproject.toml);
# }
{
dependencies = [ ]; # List of parsed PEP-508 strings (lib.pep508.parseString)
extras = {
dev = [ ]; # List of parsed PEP-508 strings (lib.pep508.parseString)
};
build-systems = [ ]; # PEP-518 build-systems (List of parsed PEP-508 strings)
}
*/
parseDependencies = pyproject: {
dependencies = map parseDependency (normalizeDependendenciesToList (pyproject.tool.poetry.dependencies or { }));
extras = mapAttrs (_: g: map parseDependency (normalizeDependendenciesToList g.dependencies)) pyproject.tool.poetry.group or { };
build-systems = pep518.parseBuildSystems pyproject;
};
/* Parse a version conditional.
Supports additional non-standard operators `^` and `~` used by Poetry.
Because some expressions desugar to multiple expressions parseVersionCond returns a list.
Type: parseVersionCond :: string -> [ AttrSet ]
*/
parseVersionCond = cond: (
let
m = match "^([~[:digit:]^])(.+)$" cond;
mAt = elemAt m;
c = mAt 0;
rest = mAt 1;
# Pad version before parsing as it's _much_ easier to reason about
# once they're the same length
version = pep440.parseVersion (lib.versions.pad 3 rest);
# Count the number of segments in the input to use an an index in ~ rewriting
segments = length (filter (tok: typeOf tok == "string") (split "\\." rest));
in
if m == null then [ (pep440.parseVersionCond cond) ]
# Desugar ~ into >= && <
else if c == "~" then [
{
op = ">=";
inherit version;
}
{
op = "<";
version = version // {
release = lib.imap0 (i: tok: if i >= segments - 1 then 0 else if i == segments - 2 then (tok + 1) else tok) version.release;
};
}
]
# Desugar ^ into >= && <
else if c == "^" then [
{
op = ">=";
inherit version;
}
{
op = "<";
version = version // {
release = rewriteCaretRhs version.release;
};
}
]
# Versions without operators are exact matches, add operator according to PEP-440
else [{
op = "==";
inherit version;
}]
);
})