Merge pull request #1358 from nix-community/pyproject-nix

Use pyproject.nix's implementations of pep508 & name normalization
This commit is contained in:
adisbladis 2023-10-26 12:55:27 +13:00 committed by GitHub
commit 6249f973b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1869 additions and 279 deletions

View file

@ -3,7 +3,13 @@
, poetryLib ? import ./lib.nix { inherit lib pkgs; inherit (pkgs) stdenv; }
}:
let
inherit (poetryLib) isCompatible readTOML normalizePackageName normalizePackageSet;
inherit (poetryLib) isCompatible readTOML;
pyproject-nix = import ./vendor/pyproject.nix { inherit lib; };
# Name normalization
inherit (pyproject-nix.pypa) normalizePackageName;
normalizePackageSet = lib.attrsets.mapAttrs' (name: value: lib.attrsets.nameValuePair (normalizePackageName name) value);
# Map SPDX identifiers to license names
spdxLicenses = lib.listToAttrs (lib.filter (pair: pair.name != null) (builtins.map (v: { name = if lib.hasAttr "spdxId" v then v.spdxId else null; value = v; }) (lib.attrValues lib.licenses)));
@ -104,7 +110,8 @@ lib.makeScope pkgs.newScope (self: {
}:
assert editablePackageSources != { };
import ./editable.nix {
inherit pyProject python pkgs lib poetryLib editablePackageSources;
inherit pyProject python pkgs lib editablePackageSources;
inherit pyproject-nix;
};
/* Returns a package containing scripts defined in tool.poetry.scripts.
@ -141,11 +148,6 @@ lib.makeScope pkgs.newScope (self: {
, extras ? [ "*" ]
}:
let
/* The default list of poetry2nix override overlays */
mkEvalPep508 = import ./pep508.nix {
inherit lib poetryLib;
inherit (python) stdenv;
};
getFunctorFn = fn: if builtins.typeOf fn == "set" then fn.__functor else fn;
scripts = pyProject.tool.poetry.scripts or { };
@ -170,12 +172,19 @@ lib.makeScope pkgs.newScope (self: {
in
lib.listToAttrs (lib.mapAttrsToList (n: v: { name = normalizePackageName n; value = v; }) lockfiles);
evalPep508 = mkEvalPep508 python;
pep508Env = pyproject-nix.pep508.mkEnviron python;
# Filter packages by their PEP508 markers & pyproject interpreter version
partitions =
let
supportsPythonVersion = pkgMeta: if pkgMeta ? marker then (evalPep508 pkgMeta.marker) else true && isCompatible (poetryLib.getPythonVersion python) pkgMeta.python-versions;
supportsPythonVersion = pkgMeta:
if pkgMeta ? marker then
(
let
marker = pyproject-nix.pep508.parseMarkers pkgMeta.marker;
in
pyproject-nix.pep508.evalMarkers pep508Env marker
) else true && isCompatible (poetryLib.getPythonVersion python) pkgMeta.python-versions;
in
lib.partition supportsPythonVersion poetryLock.package;
compatible = partitions.right;
@ -242,7 +251,8 @@ lib.makeScope pkgs.newScope (self: {
self: super:
{
mkPoetryDep = self.callPackage ./mk-poetry-dep.nix {
inherit lib python poetryLib evalPep508;
inherit lib python poetryLib pep508Env;
inherit pyproject-nix;
};
__toPluginAble = toPluginAble self;

View file

@ -1,12 +1,12 @@
{ pkgs
, lib
, poetryLib
, pyProject
, python
, editablePackageSources
, pyproject-nix
}:
let
name = poetryLib.normalizePackageName pyProject.tool.poetry.name;
name = pyproject-nix.pypa.normalizePackageName pyProject.tool.poetry.name;
# Just enough standard PKG-INFO fields for an editable installation
pkgInfoFields = {

13
lib.nix
View file

@ -8,17 +8,6 @@ let
genList (i: if i == idx then value else (builtins.elemAt list i)) (length list)
);
# Normalize package names as per PEP 503
normalizePackageName = name:
let
parts = builtins.split "[-_.]+" name;
partsWithoutSeparator = builtins.filter (x: builtins.typeOf x == "string") parts;
in
lib.strings.toLower (lib.strings.concatStringsSep "-" partsWithoutSeparator);
# Normalize an entire attrset of packages
normalizePackageSet = lib.attrsets.mapAttrs' (name: value: lib.attrsets.nameValuePair (normalizePackageName name) value);
# Get a full semver pythonVersion from a python derivation
getPythonVersion = python:
let
@ -242,8 +231,6 @@ in
getBuildSystemPkgs
satisfiesSemver
cleanPythonSources
normalizePackageName
normalizePackageSet
getPythonVersion
getTargetMachine
;

View file

@ -3,11 +3,13 @@
, python
, buildPythonPackage
, poetryLib
, evalPep508
, pep508Env
, pyproject-nix
}:
{ name
, version
, pos ? __curPos
, extras ? [ ]
, files
, source
, dependencies ? { }
@ -27,7 +29,8 @@ pythonPackages.callPackage
}@args:
let
inherit (python) stdenv;
inherit (poetryLib) isCompatible getManyLinuxDeps fetchFromLegacy fetchFromPypi normalizePackageName;
inherit (pyproject-nix.pypa) normalizePackageName;
inherit (poetryLib) isCompatible getManyLinuxDeps fetchFromLegacy fetchFromPypi;
inherit (import ./pep425.nix {
inherit lib poetryLib python stdenv;
@ -141,7 +144,16 @@ pythonPackages.callPackage
constraints = v.python or "";
pep508Markers = v.markers or "";
in
compat constraints && evalPep508 pep508Markers
compat constraints && (if pep508Markers == "" then true else
(pyproject-nix.pep508.evalMarkers
(pep508Env // {
extra = {
# All extras are always enabled
type = "extra";
value = lib.attrNames extras;
};
})
(pyproject-nix.pep508.parseMarkers pep508Markers)))
)
dependencies
);

View file

@ -1,250 +0,0 @@
{ lib, stdenv, poetryLib }: python:
let
inherit (poetryLib) ireplace;
targetMachine = poetryLib.getTargetMachine stdenv;
# Like builtins.substring but with stop being offset instead of length
substr = start: stop: s: builtins.substring start (stop - start) s;
# Strip leading/trailing whitespace from string
stripStr = s: lib.elemAt (builtins.split "^ *" (lib.elemAt (builtins.split " *$" s) 0)) 2;
findSubExpressionsFun = acc: c: (
if c == "(" then
(
let
posNew = acc.pos + 1;
isOpen = acc.openP == 0;
startPos = if isOpen then posNew else acc.startPos;
in
acc // {
inherit startPos;
exprs = acc.exprs ++ [ (substr acc.exprPos (acc.pos - 1) acc.expr) ];
pos = posNew;
openP = acc.openP + 1;
}
) else if c == ")" then
(
let
openP = acc.openP - 1;
exprs = findSubExpressions (substr acc.startPos acc.pos acc.expr);
in
acc // {
inherit openP;
pos = acc.pos + 1;
exprs = if openP == 0 then acc.exprs ++ [ exprs ] else acc.exprs;
exprPos = if openP == 0 then acc.pos + 1 else acc.exprPos;
}
) else acc // { pos = acc.pos + 1; }
);
# Make a tree out of expression groups (parens)
findSubExpressions = expr':
let
expr = " " + expr';
acc = builtins.foldl'
findSubExpressionsFun
{
exprs = [ ];
inherit expr;
pos = 0;
openP = 0;
exprPos = 0;
startPos = 0;
}
(lib.stringToCharacters expr);
tailExpr = substr acc.exprPos acc.pos expr;
tailExprs = if tailExpr != "" then [ tailExpr ] else [ ];
in
acc.exprs ++ tailExprs;
parseExpressions = exprs:
let
splitCond = s: builtins.map
(x: stripStr (if builtins.typeOf x == "list" then (builtins.elemAt x 0) else x))
(builtins.split " (and|or) " (s + " "));
mapfn = expr: (
if (builtins.match "^ ?$" expr != null) then null # Filter empty
else if (builtins.elem expr [ "and" "or" ]) then {
type = "bool";
value = expr;
}
else {
type = "expr";
value = expr;
}
);
parse = expr: builtins.filter (x: x != null) (builtins.map mapfn (splitCond expr));
in
builtins.foldl'
(
acc: v: acc ++ (if builtins.typeOf v == "string" then parse v else [ (parseExpressions v) ])
) [ ]
exprs;
# Transform individual expressions to structured expressions
# This function also performs variable substitution, replacing environment markers with their explicit values
transformExpressions = exprs:
let
variables = {
os_name =
if python.pname == "jython" then "java"
else "posix";
sys_platform =
if stdenv.isLinux then "linux"
else if stdenv.isDarwin then "darwin"
else throw "Unsupported platform";
platform_machine = targetMachine;
platform_python_implementation =
let
impl = python.passthru.implementation;
in
if impl == "cpython" then "CPython"
else if impl == "pypy" then "PyPy"
else throw "Unsupported implementation ${impl}";
platform_release = ""; # Field not reproducible
platform_system =
if stdenv.isLinux then "Linux"
else if stdenv.isDarwin then "Darwin"
else throw "Unsupported platform";
platform_version = ""; # Field not reproducible
python_version = python.passthru.pythonVersion;
python_full_version = python.version;
implementation_name = python.implementation;
implementation_version = python.version;
# extra = "";
};
substituteVar = value: if builtins.hasAttr value variables then (builtins.toJSON variables."${value}") else value;
processVar = value: builtins.foldl' (acc: v: v acc) value [
stripStr
substituteVar
];
in
if builtins.typeOf exprs == "set" then
(
if exprs.type == "expr" then
(
let
mVal = ''[a-zA-Z0-9\'"_\. \-]+'';
mOp = "in|[!=<>]+";
e = stripStr exprs.value;
m' = builtins.match ''^(${mVal}) +(${mOp}) *(${mVal})$'' e;
m = builtins.map stripStr (if m' != null then m' else builtins.match ''^(${mVal}) +(${mOp}) *(${mVal})$'' e);
m0 = processVar (builtins.elemAt m 0);
m2 = processVar (builtins.elemAt m 2);
in
{
type = "expr";
value = {
# HACK: We don't know extra at eval time, so we assume the expression is always true
op = if m0 == "extra" then "true" else builtins.elemAt m 1;
values = [ m0 m2 ];
};
}
) else exprs
) else builtins.map transformExpressions exprs;
# Recursively eval all expressions
evalExpressions = exprs:
let
unmarshal = v: (
# TODO: Handle single quoted values
if v == "True" then true
else if v == "False" then false
else builtins.fromJSON v
);
op = {
"true" = _x: _y: true;
"<=" = x: y: op.">=" y x;
"<" = x: y: lib.versionOlder (unmarshal x) (unmarshal y);
"!=" = x: y: x != y;
"==" = x: y: x == y;
">=" = x: y: lib.versionAtLeast (unmarshal x) (unmarshal y);
">" = x: y: op."<" y x;
"~=" = v: c:
let
parts = builtins.splitVersion c;
pruned = lib.take ((builtins.length parts) - 1) parts;
upper = builtins.toString (
(lib.toInt (builtins.elemAt pruned (builtins.length pruned - 1))) + 1
);
upperConstraint = builtins.concatStringsSep "." (ireplace (builtins.length pruned - 1) upper pruned);
in
op.">=" v c && op."<" v upperConstraint;
"===" = x: y: x == y;
"in" = x: y:
let
values = builtins.filter (x: builtins.typeOf x == "string") (builtins.split " " (unmarshal y));
in
builtins.elem (unmarshal x) values;
};
in
if builtins.typeOf exprs == "set" then
(
if exprs.type == "expr" then
(
let
expr = exprs;
result = op."${expr.value.op}" (builtins.elemAt expr.value.values 0) (builtins.elemAt expr.value.values 1);
in
{
type = "value";
value = result;
}
) else exprs
) else builtins.map evalExpressions exprs;
# Now that we have performed an eval all that's left to do is to concat the graph into a single bool
reduceExpressions = exprs:
let
cond = {
"and" = x: y: x && y;
"or" = x: y: x || y;
};
reduceExpressionsFun = acc: v: (
if builtins.typeOf v == "set" then
(
if v.type == "value" then
(
acc // {
value = cond."${acc.cond}" acc.value v.value;
}
) else if v.type == "bool" then
(
acc // {
cond = v.value;
}
) else throw "Unsupported type"
) else if builtins.typeOf v == "list" then
(
let
ret = builtins.foldl'
reduceExpressionsFun
{
value = true;
cond = "and";
}
v;
in
acc // {
value = cond."${acc.cond}" acc.value ret.value;
}
) else throw "Unsupported type"
);
in
(
builtins.foldl'
reduceExpressionsFun
{
value = true;
cond = "and";
}
exprs
).value;
in
e: builtins.foldl' (acc: v: v acc) e [
findSubExpressions
parseExpressions
transformExpressions
evalExpressions
reduceExpressions
]

View file

@ -1,5 +1,5 @@
[tool.poetry]
name = "test_sqlalchemy"
name = "inlist"
version = "0.1.0"
description = "poetry2nix test"
authors = ["Your Name <you@example.com>"]

22
vendor/pyproject.nix/default.nix vendored Normal file
View file

@ -0,0 +1,22 @@
{ lib }:
let
inherit (builtins) mapAttrs;
inherit (lib) fix;
in
fix (self: mapAttrs (_: path: import path ({ inherit lib; } // self)) {
pip = ./pip.nix;
pypa = ./pypa.nix;
project = ./project.nix;
renderers = ./renderers.nix;
validators = ./validators.nix;
poetry = ./poetry.nix;
pep427 = ./pep427.nix;
pep440 = ./pep440.nix;
pep508 = ./pep508.nix;
pep518 = ./pep518.nix;
pep599 = ./pep599.nix;
pep600 = ./pep600.nix;
pep621 = ./pep621.nix;
})

49
vendor/pyproject.nix/pep427.nix vendored Normal file
View file

@ -0,0 +1,49 @@
_:
let
inherit (builtins) match elemAt split filter isString;
matchFileName = match "([^-]+)-([^-]+)(-([[:digit:]][^-]*))?-([^-]+)-([^-]+)-(.+).whl";
in
{
/* Check whether string is a wheel file or not.
Type: isWheelFileName :: string -> bool
Example:
# isWheelFileName "cryptography-41.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"
true
*/
isWheelFileName = name: matchFileName name != null;
/* Parse PEP-427 wheel file names.
Type: parseFileName :: string -> AttrSet
Example:
# parseFileName "cryptography-41.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"
{
abiTag = "abi3";
buildTag = null;
distribution = "cryptography";
languageTag = "cp37";
platformTags = [ "manylinux_2_17_aarch64" "manylinux2014_aarch64" ];
version = "41.0.1";
}
*/
parseFileName =
# The wheel filename is `{distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl`.
name:
let
m = matchFileName name;
mAt = elemAt m;
in
assert m != null; {
distribution = mAt 0;
version = mAt 1;
buildTag = mAt 3;
languageTag = mAt 4;
abiTag = mAt 5;
platformTags = filter isString (split "\\." (mAt 6));
};
}

261
vendor/pyproject.nix/pep440.nix vendored Normal file
View file

@ -0,0 +1,261 @@
{ lib, ... }:
let
inherit (builtins) split filter match length elemAt head tail foldl' fromJSON typeOf;
inherit (lib) fix isString toInt toLower sublist;
filterNull = filter (x: x != null);
filterEmpty = filter (x: length x > 0);
filterEmptyStr = filter (s: s != "");
# A version of lib.toInt that supports leading zeroes
toIntRelease = s:
let
n = fromJSON (head (match "0?([[:digit:]]+)" s));
in
assert typeOf n == "int"; n;
# Return a list elem at index with a default value if it doesn't exist
optionalElem = list: idx: default: if length list >= idx + 1 then elemAt list idx else default;
# We consider some words to be alternate spellings of other words and
# in those cases we want to normalize the spellings to our preferred
# spelling.
normalizedReleaseTypes = {
alpha = "a";
beta = "b";
c = "rc";
pre = "rc";
preview = "rc";
rev = "post";
r = "post";
"-" = "post";
};
# Parse a release (pre/post/whatever) attrset from split tokens
parseReleaseSuffix = patterns: tokens:
let
matches = map
(x:
let
type = toLower (elemAt x 0);
value = elemAt x 1;
in
{
type = normalizedReleaseTypes.${type} or type;
value = if value != "" then toInt value else 0;
})
(filterNull (map (match "[0-9]*(${patterns})([0-9]*)") tokens));
in
assert length matches <= 1; optionalElem matches 0 null;
parsePre = parseReleaseSuffix "a|b|c|rc|alpha|beta|pre|preview";
parsePost = parseReleaseSuffix "post|rev|r|\-";
parseDev = parseReleaseSuffix "dev";
parseLocal = parseReleaseSuffix "\\+";
# Compare the release fields from the parsed version
compareRelease = ra: rb:
let
x = head ra;
y = head rb;
in
if length ra == 0 || length rb == 0 then 0 else
(
if x == "*" || y == "*" then 0 # Wildcards are always considered equal
else
(
if x > y then 1
else if x < y then -1
else compareRelease (tail ra) (tail rb)
)
);
# Normalized modifier to it's priority (in case we are comparing an alpha to a beta or similar)
modifierPriority = {
dev = -1;
a = 0;
b = 1;
rc = 2;
post = 3;
};
# Compare dev/pre/post/local release modifiers
compareVersionModifier = x: y: assert x != null && y != null; let
prioX = modifierPriority.${x.type};
prioY = modifierPriority.${y.type};
in
if prioX == prioY then
(
if x.value == y.value then 0
else if x.value > y.value then 1
else -1
)
else if prioX > prioY then 1
else 0;
in
fix (self: {
/* Parse a version according to PEP-440.
Type: parseVersion :: string -> AttrSet
Example:
# parseVersion "3.0.0rc1"
{
dev = null;
epoch = 0;
local = null;
post = null;
pre = {
type = "rc";
value = 1;
};
release = [ 3 0 0 ];
}
*/
parseVersion = version:
let
tokens = filter isString (split "\\." version);
in
{
# Return epoch defaulting to 0
epoch = toInt (optionalElem (map head (filterNull (map (match "[0-9]+!([0-9]+)") tokens))) 0 "0");
release = map (t: (x: if x == "*" then x else toIntRelease x) (head t)) (filterEmpty (map (t: filterEmptyStr (match "([\\*0-9]*).*" t)) tokens));
pre = parsePre tokens;
post = parsePost tokens;
dev = parseDev tokens;
local = parseLocal tokens;
};
/* Parse a version conditional.
Type: parseVersionCond :: string -> AttrSet
Example:
# parseVersionCond ">=3.0.0rc1"
{
op = ">=";
version = {
dev = null;
epoch = 0;
local = null;
post = null;
pre = {
type = "rc";
value = 1;
};
release = [ 3 0 0 ];
};
}
*/
parseVersionCond = cond: (
let
m = match " *([=><!~^]+) *(.+)" cond;
mAt = elemAt m;
in
{
op = mAt 0;
version = self.parseVersion (mAt 1);
}
);
/* Compare two versions as parsed by `parseVersion` according to PEP-440.
Returns:
- -1 for less than
- 0 for equality
- 1 for greater than
Type: compareVersions :: AttrSet -> AttrSet -> int
Example:
# compareVersions (parseVersion "3.0.0") (parseVersion "3.0.0")
0
*/
compareVersions = a: b: foldl' (acc: comp: if acc != 0 then acc else comp) 0 [
# mixing dev/pre/post like:
# 1.0b2.post345.dev456
# 1.0b2.post345
# is valid and we need to consider them all.
# Compare release field
(compareRelease a.release b.release)
# Compare pre release
(
if a.pre != null && b.pre != null then compareVersionModifier a.pre b.pre
else if a.pre != null then -1
else if b.pre != null then 1
else 0
)
# Compare dev release
(
if a.dev != null && b.dev != null then compareVersionModifier a.dev b.dev
else if a.dev != null then -1
else if b.dev != null then 1
else 0
)
# Compare post release
(
if a.post != null && b.post != null then compareVersionModifier a.post b.post
else if a.post != null then 1
else if b.post != null then -1
else 0
)
# Compare epoch
(
if a.epoch == b.epoch then 0
else if a.epoch > b.epoch then 1
else -1
)
# Compare local
(
if a.local != null && b.local != null then compareVersionModifier a.local b.local
else if b.local != null then -1
else 0
)
];
/* Map comparison operators as strings to a comparator function.
Attributes:
- [Compatible release clause](https://peps.python.org/pep-0440/#compatible-release): `~=`
- [Version matching clause](https://peps.python.org/pep-0440/#version-matching): `==`
- [Version exclusion clause](https://peps.python.org/pep-0440/#version-exclusion): `!=`
- [Inclusive ordered comparison clause](https://peps.python.org/pep-0440/#inclusive-ordered-comparison): `<=`, `>=`
- [Exclusive ordered comparison clause](https://peps.python.org/pep-0440/#exclusive-ordered-comparison): `<`, `>`
- [Arbitrary equality clause](https://peps.python.org/pep-0440/#arbitrary-equality): `===`
Type: operators.${operator} :: AttrSet -> AttrSet -> bool
Example:
# comparators."==" (parseVersion "3.0.0") (parseVersion "3.0.0")
true
*/
comparators = {
"~=" = a: b: (
# Local version identifiers are NOT permitted in this version specifier.
assert a.local == null && b.local == null;
self.comparators.">=" a b && self.comparators."==" a (b // {
release = sublist 0 ((length b.release) - 1) b.release;
# If a pre-release, post-release or developmental release is named in a compatible release clause as V.N.suffix, then the suffix is ignored when determining the required prefix match.
pre = null;
post = null;
dev = null;
})
);
"==" = a: b: self.compareVersions a b == 0;
"!=" = a: b: self.compareVersions a b != 0;
"<=" = a: b: self.compareVersions a b <= 0;
">=" = a: b: self.compareVersions a b >= 0;
"<" = a: b: self.compareVersions a b < 0;
">" = a: b: self.compareVersions a b > 0;
"===" = throw "Arbitrary equality clause not supported";
};
})

544
vendor/pyproject.nix/pep508.nix vendored Normal file
View file

@ -0,0 +1,544 @@
{ lib, pep440, pep599, pypa, ... }:
let
inherit (builtins) match elemAt split foldl' substring stringLength typeOf fromJSON isString head mapAttrs elem length;
inherit (lib) stringToCharacters fix;
inherit (import ./util.nix { inherit lib; }) splitComma;
re = {
operators = "([=><!~^]+)";
version = "([0-9.*x]+)";
};
# Assign numerical priority values to logical conditions so we can do proper precedence ordering
condPrio = {
and = 5;
or = 10;
not = 1;
"in" = -1;
"not in" = -1;
"" = -1;
};
condGt = l: r: if l == "" then false else condPrio.${l} >= condPrio.${r};
# Parse a value into an attrset of { type = "valueType"; value = ...; }
# Will parse any field name suffixed with "version" as a PEP-440 version, otherwise
# the value is passed through and the type is inferred with builtins.typeOf
parseValueVersionDynamic = name: value: (
if match "^.+version" name != null && isString value then {
type = "version";
value = pep440.parseVersion value;
} else {
type = typeOf value;
inherit value;
}
);
# Strip leading/trailing whitespace from string
stripStr = s: let t = match "[\t ]*(.*[^\t ])[\t ]*" s; in if t == null then "" else head t;
# Remove groupings ( ) from expression
unparen = expr':
let
expr = stripStr expr';
m = match "\\((.+)\\)" expr;
in
if m != null then elemAt m 0 else expr;
isMarkerVariable =
let
markerFields = [
"implementation_name"
"implementation_version"
"os_name"
"platform_machine"
"platform_python_implementation"
"platform_release"
"platform_system"
"platform_version"
"python_full_version"
"python_version"
"sys_platform"
"extra"
];
in
s: elem s markerFields;
unpackValue = value:
let
# If the value is a single ticked string we can't pass it plainly to toJSON.
# Normalise to a double quoted.
singleTicked = match "^'(.+)'$" value; # TODO: Account for escaped ' in input (unescape)
in
if isMarkerVariable value then value
else fromJSON (if singleTicked != null then "\"" + head singleTicked + "\"" else value);
# Comparators for simple equality
# For versions see pep440.comparators
comparators = {
"==" = a: b: a == b;
"!=" = a: b: a != b;
"<=" = a: b: a <= b;
">=" = a: b: a >= b;
"<" = a: b: a < b;
">" = a: b: a > b;
"===" = a: b: a == b;
};
# Special case comparators for the `extra` environment field
extraComparators = {
# Check for member in list if list, otherwise simply compare.
"==" = extras: extra: if typeOf extras == "list" then elem extra extras else extras == extra;
"!=" = extras: extra: if typeOf extras == "list" then !(elem extra extras) else extras != extra;
};
boolOps = {
"and" = x: y: x && y;
"or" = x: y: x || y;
"in" = x: y: lib.strings.hasInfix x y;
};
isPrimitiveType =
let
primitives = [
"int"
"float"
"string"
"bool"
];
in
type: elem type primitives;
in
fix (self:
{
/* Parse PEP 508 markers into an AST.
Type: parseMarkers :: string -> AttrSet
Example:
# parseMarkers "(os_name=='a' or os_name=='b') and os_name=='c'"
{
lhs = {
lhs = {
lhs = {
type = "variable";
value = "os_name";
};
op = "==";
rhs = {
type = "string";
value = "a";
};
type = "compare";
};
op = "or";
rhs = {
lhs = {
type = "variable";
value = "os_name";
};
op = "==";
rhs = {
type = "string";
value = "b";
};
type = "compare";
};
type = "boolOp";
};
op = "and";
rhs = {
lhs = {
type = "variable";
value = "os_name";
};
op = "==";
rhs = {
type = "string";
value = "c";
};
type = "compare";
};
type = "boolOp";
}
*/
parseMarkers = input:
let
# Find the positions of lhs/op/rhs in the input string
pos = foldl'
(acc: c:
let
# # Look ahead to find the operator (either "and", "not" or "or").
cond =
if self.openP > 0 || acc.inString then ""
else if substring acc.pos 5 input == " and " then "and"
else if substring acc.pos 4 input == " or " then "or"
else if substring acc.pos 4 input == " in " then "in"
else if substring acc.pos 8 input == " not in " then "not in"
else if substring acc.pos 5 input == " not " then "not"
else "";
# When we've reached the operator we know the start/end positions of lhs/op/rhs
rhsOffset =
if cond != "" && condGt cond acc.cond then
(
if (cond == "and" || cond == "not") then 5
else if (cond == "or" || cond == "in") then 4
else if cond == "not in" then 8
else throw "Unknown cond: ${cond}"
) else -1;
self = {
# If we are inside a string don't track the opening and closing of parens
openP = if acc.inString then acc.openP else
(
if c == "(" then acc.openP + 1
else if c == ")" then acc.openP - 1
else acc.openP
);
# Check opening and closing of strings
inString =
if acc.inString && c == "'" then true
else if !acc.inString && c == "'" then false
else acc.inString;
pos = acc.pos + 1;
cond = if cond != "" then cond else acc.cond;
lhs = if (rhsOffset != -1) then acc.pos else acc.lhs;
rhs = if (rhsOffset != -1) then (acc.pos + rhsOffset) else acc.rhs;
};
in
self)
{
openP = 0; # Number of open parens
inString = false; # If the parser is inside a string
pos = 0; # Parser position
done = false;
# Keep track of last logical condition to do precedence ordering
cond = "";
# Stop positions for each value
lhs = -1;
rhs = -1;
}
(stringToCharacters input);
in
if pos.lhs == -1 then # No right hand value to extract
(
let
m = split re.operators (unparen input);
mLength = length m;
mAt = elemAt m;
lhs = stripStr (mAt 0);
in
if (mLength > 1) then assert mLength == 3; {
type = "compare";
lhs =
if isMarkerVariable lhs then {
type = "variable";
value = lhs;
} else unpackValue lhs;
op = elemAt (mAt 1) 0;
rhs = parseValueVersionDynamic lhs (unpackValue (stripStr (mAt 2)));
} else if isMarkerVariable input then {
type = "variable";
value = input;
} else rec {
value = unpackValue input;
type = typeOf value;
}
) else {
type = "boolOp";
lhs = self.parseMarkers (unparen (substring 0 pos.lhs input));
op = substring (pos.lhs + 1) (pos.rhs - pos.lhs - 2) input;
rhs = self.parseMarkers (unparen (substring pos.rhs (stringLength input) input));
};
/* Parse a PEP-508 dependency string.
Type: parseString :: string -> AttrSet
Example:
# parseString "cachecontrol[filecache]>=0.13.0"
{
conditions = [
{
op = ">=";
version = {
dev = null;
epoch = 0;
local = null;
post = null;
pre = null;
release = [ 0 13 0 ];
};
}
];
markers = null;
name = "cachecontrol";
extras = [ "filecache" ];
url = null;
}
*/
parseString = input:
let
# Split the input into it's distinct parts: The package segment, URL and environment markers
tokens =
let
# Input has both @ and ; separators (both URL and markers)
# "name [fred,bar] @ http://foo.com ; python_version=='2.7'"
m1 = match "^(.+)@(.+);(.+)$" input;
# Input has ; separator (markers)
# "name [fred,bar] ; python_version=='2.7'"
m2 = match "^(.+);(.+)$" input;
# Input has @ separator (URL)
# "name [fred,bar] @ http://foo.com"
m3 = match "^(.+)@(.+)$" input;
in
if m1 != null then {
packageSegment = elemAt m1 0;
url = stripStr (elemAt m1 1);
markerSegment = elemAt m1 2;
}
else if m2 != null then {
packageSegment = elemAt m2 0;
url = null;
markerSegment = elemAt m2 1;
}
else if m3 != null then {
packageSegment = elemAt m3 0;
url = stripStr (elemAt m3 1);
markerSegment = null;
}
else
(
if match ".+\/.+" input != null then
# Input is a bare URL
{
packageSegment = null;
url = input;
markerSegment = null;
} else
# Input is a package name
{
packageSegment = input;
url = null;
markerSegment = null;
}
);
# Extract metadata from the package segment
package =
let
# Package has either both extras and version constraints or just extras
# "name [fred,bar]>=3.10"
# "name [fred,bar]"
m1 = match "(.+)\\[(.*)](.*)" tokens.packageSegment;
# Package has either version constraints or is bare
# "name>=3.2"
# "name"
m2 = match "([a-zA-Z0-9_\\.-]+)(.*)" tokens.packageSegment;
# The version conditions as a list of strings
conditions = map pep440.parseVersionCond (splitComma (if m1 != null then elemAt m1 2 else elemAt m2 1));
# Extras as a list of strings
#
# Based on PEP-508 alone it's not clear whether extras should be normalized or not.
# From discussion in https://github.com/pypa/packaging-problems/issues/230
# missing normalization seems like an oversight.
extras = if m1 != null then map pypa.normalizePackageName (splitComma (elemAt m1 1)) else [ ];
in
if tokens.packageSegment == null then {
name = null;
conditions = [ ];
extras = [ ];
} else
# Assert that either regex matched
assert m1 != null || m2 != null; {
# Based on PEP-508 alone it's not clear whether names should be normalized or not.
# From discussion in https://github.com/pypa/packaging-problems/issues/230
# this seems like an oversight and we _should_ actually canonicalize names at parse time.
name = pypa.normalizePackageName (stripStr (if m1 != null then elemAt m1 0 else elemAt m2 0));
inherit extras conditions;
};
in
{
inherit (package) name conditions extras;
inherit (tokens) url;
markers = if tokens.markerSegment == null then null else self.parseMarkers tokens.markerSegment;
};
/* Create an attrset of platform variables.
As described in https://peps.python.org/pep-0508/#environment-markers.
Type: mkEnviron :: derivation -> AttrSet
Example:
# mkEnviron pkgs.python3
{
implementation_name = {
type = "string";
value = "cpython";
};
implementation_version = {
type = "version";
value = {
dev = null;
epoch = 0;
local = null;
post = null;
pre = null;
release = [ 3 10 12 ];
};
};
os_name = {
type = "string";
value = "posix";
};
platform_machine = {
type = "string";
value = "x86_64";
};
platform_python_implementation = {
type = "string";
value = "CPython";
};
# platform_release maps to platform.release() which returns
# the running kernel version on Linux.
# Because this field is not reproducible it's left empty.
platform_release = {
type = "string";
value = "";
};
platform_system = {
type = "string";
value = "Linux";
};
# platform_version maps to platform.version() which also returns
# the running kernel version on Linux.
# Because this field is not reproducible it's left empty.
platform_version = {
type = "version";
value = {
dev = null;
epoch = 0;
local = null;
post = null;
pre = null;
release = [ ];
};
};
python_full_version = {
type = "version";
value = {
dev = null;
epoch = 0;
local = null;
post = null;
pre = null;
release = [ 3 10 12 ];
};
};
python_version = {
type = "version";
value = {
dev = null;
epoch = 0;
local = null;
post = null;
pre = null;
release = [ 3 10 ];
};
};
sys_platform = {
type = "string";
value = "linux";
};
}
*/
mkEnviron = python:
let
inherit (python) stdenv;
targetMachine = pep599.manyLinuxTargetMachines.${stdenv.targetPlatform.parsed.cpu.name} or null;
in
mapAttrs
parseValueVersionDynamic
{
os_name =
if python.pname == "jython" then "java"
else "posix";
sys_platform =
if stdenv.isLinux then "linux"
else if stdenv.isDarwin then "darwin"
else throw "Unsupported platform";
platform_machine = targetMachine;
platform_python_implementation =
let
impl = python.passthru.implementation;
in
if impl == "cpython" then "CPython"
else if impl == "pypy" then "PyPy"
else throw "Unsupported implementation ${impl}";
platform_release = ""; # Field not reproducible
platform_system =
if stdenv.isLinux then "Linux"
else if stdenv.isDarwin then "Darwin"
else throw "Unsupported platform";
platform_version = ""; # Field not reproducible
python_version = python.passthru.pythonVersion;
python_full_version = python.version;
implementation_name = python.passthru.implementation;
implementation_version = python.version;
};
/* Evaluate an environment as returned by `mkEnviron` against markers as returend by `parseMarkers`.
Type: evalMarkers :: AttrSet -> AttrSet -> bool
Example:
# evalMarkers (mkEnviron pkgs.python3) (parseMarkers "python_version < \"3.11\"")
true
*/
evalMarkers = environ: value: (
let
x = self.evalMarkers environ value.lhs;
y = self.evalMarkers environ value.rhs;
in
if value.type == "compare" then
(
(
# Version comparison
if value.lhs.type == "version" || value.rhs.type == "version" then pep440.comparators.${value.op}
# `Extra` environment marker comparison requires special casing because it's equality checks can
# == can be considered a `"key" in set` comparison when multiple extras are activated for a dependency.
# If we didn't treat it this way the check would become quadratic as `evalMarkers` only could check one extra at a time.
else if value.lhs.type == "variable" || value.lhs.value == "extra" then extraComparators.${value.op}
# Simple equality
else comparators.${value.op}
) x
y
)
else if value.type == "boolOp" then boolOps.${value.op} x y
else if value.type == "variable" then (self.evalMarkers environ environ.${value.value})
else if value.type == "version" || value.type == "extra" then value.value
else if isPrimitiveType value.type then value.value
else throw "Unknown type '${value.type}'"
);
})

12
vendor/pyproject.nix/pep518.nix vendored Normal file
View file

@ -0,0 +1,12 @@
{ pep508, ... }:
{
/* Parse PEP-518 `build-system.requires` from pyproject.toml.
Type: readPyproject :: AttrSet -> list
Example:
# parseBuildSystems (lib.importTOML ./pyproject.toml)
[ ] # List of parsed PEP-508 strings as returned by `lib.pep508.parseString`.
*/
parseBuildSystems = pyproject: map pep508.parseString (pyproject.build-system.requires or [ ]);
}

20
vendor/pyproject.nix/pep599.nix vendored Normal file
View file

@ -0,0 +1,20 @@
_:
{
/* Map Nixpkgs CPU values to target machines known to be supported for manylinux* wheels (a.k.a. `uname -m`),
in nixpkgs found under the attribute `stdenv.targetPlatform.parsed.cpu.name`
Example:
# legacyAliases.powerpc64
"ppc64"
*/
manyLinuxTargetMachines = {
x86_64 = "x86_64";
i686 = "i686";
aarch64 = "aarch64";
armv7l = "armv7l";
powerpc64 = "ppc64";
powerpc64le = "ppc64le";
s390x = "s390x";
};
}

58
vendor/pyproject.nix/pep600.nix vendored Normal file
View file

@ -0,0 +1,58 @@
{ lib, pep599, ... }:
let
inherit (builtins) match elemAt compareVersions splitVersion;
inherit (lib) fix;
in
fix (self: {
/* Map legacy (pre PEP-600) platform tags to PEP-600 compliant ones.
https://peps.python.org/pep-0600/#legacy-manylinux-tags
Type: legacyAliases.${tag} :: AttrSet -> string
Example:
# legacyAliases."manylinux1_x86_64" or "manylinux1_x86_64"
"manylinux_2_5_x86_64"
*/
legacyAliases = {
manylinux1_x86_64 = "manylinux_2_5_x86_64";
manylinux1_i686 = "manylinux_2_5_i686";
manylinux2010_x86_64 = "manylinux_2_12_x86_64";
manylinux2010_i686 = "manylinux_2_12_i686";
manylinux2014_x86_64 = "manylinux_2_17_x86_64";
manylinux2014_i686 = "manylinux_2_17_i686";
manylinux2014_aarch64 = "manylinux_2_17_aarch64";
manylinux2014_armv7l = "manylinux_2_17_armv7l";
manylinux2014_ppc64 = "manylinux_2_17_ppc64";
manylinux2014_ppc64le = "manylinux_2_17_ppc64le";
manylinux2014_s390x = "manylinux_2_17_s390x";
};
/* Check if a manylinux tag is compatible with a given stdenv.
Type: manyLinuxTagCompatible :: AttrSet -> string -> bool
Example:
# manyLinuxTagCompatible pkgs.stdenv "manylinux_2_5_x86_64"
true
*/
manyLinuxTagCompatible = stdenv: tag:
let
tag' = self.legacyAliases.${tag} or tag;
m = match "manylinux_([0-9]+)_([0-9]+)_(.*)" tag';
mAt = elemAt m;
tagMajor = mAt 0;
tagMinor = mAt 1;
tagArch = mAt 2;
sysVersion' = elemAt (splitVersion stdenv.cc.libc.version);
sysMajor = sysVersion' 0;
sysMinor = sysVersion' 1;
in
if m == null then throw "'${tag'}' is not a valid manylinux tag."
else if stdenv.cc.libc.pname != "glibc" then false
else if compareVersions "${sysMajor}.${sysMinor}" "${tagMajor}.${tagMinor}" < 0 then false
else if pep599.manyLinuxTargetMachines.${tagArch} != stdenv.targetPlatform.parsed.cpu.name then false
else true;
})

147
vendor/pyproject.nix/pep621.nix vendored Normal file
View file

@ -0,0 +1,147 @@
{ lib, pep440, pep508, pep518, ... }:
let
inherit (builtins) mapAttrs foldl' split filter elem;
inherit (lib) isString filterAttrs fix;
splitAttrPath = path: filter isString (split "\\." path);
getAttrPath = path: lib.getAttrFromPath (splitAttrPath path);
in
fix (self: {
/* Parse dependencies from pyproject.toml.
Type: parseDependencies :: AttrSet -> AttrSet
Example:
# parseDependencies {
#
# pyproject = (lib.importTOML ./pyproject.toml);
# # Don't just look at `project.optional-dependencies` for groups, also look at these:
# extrasAttrPaths = [ "tool.pdm.dev-dependencies" ];
# }
{
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, extrasAttrPaths ? [ ] }:
let
# Fold extras from all considered attributes into one set
extras' = foldl' (acc: attr: acc // getAttrPath attr pyproject) (pyproject.project.optional-dependencies or { }) extrasAttrPaths;
in
{
dependencies = map pep508.parseString (pyproject.project.dependencies or [ ]);
extras = mapAttrs (_: map pep508.parseString) extras';
build-systems = pep518.parseBuildSystems pyproject;
};
/* Parse project.python-requires from pyproject.toml
Type: parseRequiresPython :: AttrSet -> list
Example:
# parseRequiresPython (lib.importTOML ./pyproject.toml)
[ ] # List of conditions as returned by `lib.pep440.parseVersionCond`
*/
parseRequiresPython = pyproject: map pep440.parseVersionCond (filter isString (split "," (pyproject.project.requires-python or "")));
/* Takes a dependency structure as returned by `lib.pep621.parseDependencies` and transforms it into
a structure with it's package names.
Type: getDependenciesNames :: AttrSet -> AttrSet
Example:
# getDependenciesNames (pep621.parseDependencies { pyproject = (lib.importTOML ./pyproject.toml); })
{
dependencies = [ "requests" ];
extras = {
dev = [ "pytest" ];
};
build-systems = [ "poetry-core" ];
}
*/
getDependenciesNames =
let
getNames = map (dep: dep.name);
in
dependencies: {
dependencies = getNames dependencies.dependencies;
extras = mapAttrs (_: getNames) dependencies.extras;
build-systems = getNames dependencies.build-systems;
};
/* Filter dependencies not relevant for this environment.
Type: filterDependenciesByEnviron :: AttrSet -> AttrSet -> AttrSet
Example:
# filterDependenciesByEnviron (lib.pep508.mkEnviron pkgs.python3) (lib.pep621.parseDependencies (lib.importTOML ./pyproject.toml))
{ } # Structure omitted in docs
*/
filterDependenciesByEnviron =
# Environ as created by `lib.pep508.mkEnviron`.
environ:
# Extras as a list of strings
extras:
# Dependencies as parsed by `lib.pep621.parseDependencies`.
dependencies:
(
let
filterList = environ: filter (dep: dep.markers == null || pep508.evalMarkers
(environ // {
extra = {
type = "extra";
value = extras;
};
})
dep.markers);
in
{
dependencies = filterList environ dependencies.dependencies;
extras = mapAttrs (_: filterList environ) dependencies.extras;
build-systems = filterList environ dependencies.build-systems;
}
);
/* Filter dependencies by their extras groups.
Type: filterDependenciesByExtras :: list[string] -> AttrSet -> AttrSet
Example:
# filterDependenciesByExtras [ "dev" ] (lib.pep621.parseDependencies (lib.importTOML ./pyproject.toml))
{ } # Structure omitted in docs
*/
filterDependenciesByExtras =
# Extras groups as a list of strings.
extras:
# Dependencies as parsed by `lib.pep621.parseDependencies`.
dependencies:
dependencies // {
extras = filterAttrs (group: _: elem group extras) dependencies.extras;
};
/* Aggregate of `filterDependencies` & `filterDependenciesByExtras`
Type: filterDependencies :: AttrSet -> AttrSet
Example:
# filterDependencies {
# dependencies = lib.pep621.parseDependencies (lib.importTOML ./pyproject.toml);
# environ = lib.pep508.mkEnviron pkgs.python;
# extras = [ "dev" ];
# }
{ } # Structure omitted in docs
*/
filterDependencies =
{
# Dependencies as parsed by `lib.pep621.parseDependencies`
dependencies
, # Environ as created by `lib.pep508.mkEnviron`
environ
, # Extras as a list of strings
extras ? [ ]
,
}: self.filterDependenciesByEnviron environ extras (self.filterDependenciesByExtras extras dependencies);
})

105
vendor/pyproject.nix/pip.nix vendored Normal file
View file

@ -0,0 +1,105 @@
{ lib
, pep508
, ...
}:
let
inherit (builtins) match head tail typeOf split filter foldl' readFile dirOf hasContext unsafeDiscardStringContext;
stripStr = s:
let
t = match "[\t ]*(.*[^\t ])[\t ]*" s;
in
if t == null
then ""
else head t;
uncomment = l: head (match " *([^#]*).*" l);
in
lib.fix (self: {
/* Parse dependencies from requirements.txt
Type: parseRequirementsTxt :: AttrSet -> list
Example:
# parseRequirements ./requirements.txt
[ { flags = []; requirement = {}; # Returned by pep508.parseString } ]
*/
parseRequirementsTxt =
# The contents of or path to requirements.txt
requirements:
let
# Paths are either paths or strings with context.
# Preferably we'd just use paths but because of
#
# $ ./. + requirements
# "a string that refers to a store path cannot be appended to a path"
#
# We also need to support stringly paths...
isPath = typeOf requirements == "path" || hasContext requirements;
path' =
if typeOf requirements == "path" then requirements else
/. + unsafeDiscardStringContext requirements;
root = dirOf path';
# Requirements without comments and no empty strings
requirements' = if isPath then readFile path' else requirements;
lines' = filter (l: l != "") (map uncomment (filter (l: typeOf l == "string") (split "\n" requirements')));
# Fold line continuations
inherit ((foldl'
(
acc: l':
let
m = match "(.+) *\\\\" l';
continue = m != null;
l = stripStr (
if continue
then (head m)
else l'
);
in
if continue
then {
line = acc.line ++ [ l ];
inherit (acc) lines;
}
else {
line = [ ];
lines = acc.lines ++ [ (acc.line ++ [ l ]) ];
}
)
{
lines = [ ];
line = [ ];
}
lines')) lines;
in
foldl'
(acc: l:
let
m = match "-(c|r) (.+)" (head l);
in
acc ++ (
# Common case, parse string
if m == null
then [{
requirement = pep508.parseString (head l);
flags = tail l;
}]
# Don't support constraint files
else if (head m) == "c" then throw "Unsupported flag: -c"
# Recursive requirements.txt
else
(self.parseRequirementsTxt (
if root == null then throw "When importing recursive requirements.txt requirements needs to be passed as a path"
else root + "/${head (tail m)}"
))
))
[ ]
lines;
})

249
vendor/pyproject.nix/poetry.nix vendored Normal file
View file

@ -0,0 +1,249 @@
{ lib
, pep440
, pep508
, pep518
, ...
}:
let
inherit (builtins) match elemAt foldl' typeOf attrNames head tail mapAttrs;
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);
# Supports additional non-standard operators `^` and `~` used by Poetry.
# Other operators are passed through to pep440.
# Because some expressions desugar to multiple expressions parseVersionCond returns a list.
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);
in
if m == null then [ (pep440.parseVersionCond cond) ]
# Desugar ~ into >= && <
else if c == "~" then [
{
cond = ">=";
inherit version;
}
{
cond = "<";
version = version // {
release = [ (head version.release + 1) ] ++ tail version.release;
};
}
]
# Desugar ^ into >= && <
else if c == "^" then [
{
cond = ">=";
inherit version;
}
{
cond = "<";
version = version // {
release = rewriteCaretRhs version.release;
};
}
]
# Versions without operators are exact matches, add operator according to PEP-440
else [{
cond = "==";
inherit version;
}]
);
# Normalized version of parseVersionCond'
parseVersionConds = s: flatten (map 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)
}
*/
# # Analogous to
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;
};
}

82
vendor/pyproject.nix/project.nix vendored Normal file
View file

@ -0,0 +1,82 @@
{ pep518, pep621, poetry, pip, ... }:
{
/* Load dependencies from a pyproject.toml.
Type: loadPyproject :: AttrSet -> AttrSet
Example:
# loadPyproject { pyproject = lib.importTOML }
{
dependencies = { }; # Parsed dependency structure in the schema of `lib.pep621.parseDependencies`
build-systems = [ ]; # Returned by `lib.pep518.parseBuildSystems`
pyproject = { }; # The unmarshaled contents of pyproject.toml
}
*/
loadPyproject =
{
# The unmarshaled contents of pyproject.toml
pyproject
# Example: extrasAttrPaths = [ "tool.pdm.dev-dependencies" ];
, extrasAttrPaths ? [ ]
}: {
dependencies = pep621.parseDependencies { inherit pyproject extrasAttrPaths; };
build-systems = pep518.parseBuildSystems pyproject;
inherit pyproject;
};
/* Load dependencies from a Poetry pyproject.toml.
Type: loadPoetryPyproject :: AttrSet -> AttrSet
Example:
# loadPoetryPyproject { pyproject = lib.importTOML }
{
dependencies = { }; # Parsed dependency structure in the schema of `lib.pep621.parseDependencies`
build-systems = [ ]; # Returned by `lib.pep518.parseBuildSystems`
pyproject = { }; # The unmarshaled contents of pyproject.toml
}
*/
loadPoetryPyproject =
{
# The unmarshaled contents of pyproject.toml
pyproject
}:
let
pyproject-pep621 = poetry.translatePoetryProject pyproject;
in
{
dependencies = poetry.parseDependencies pyproject;
build-systems = pep518.parseBuildSystems pyproject;
pyproject = pyproject-pep621;
pyproject-poetry = pyproject;
};
/* Load dependencies from a requirements.txt.
Note that as requirements.txt is lacking important project metadata this is incompatible with some renderers.
Type: loadRequirementsTxt :: AttrSet -> AttrSet
Example:
# loadRequirementstxt { requirements = builtins.readFile ./requirements.txt; root = ./.; }
{
dependencies = { }; # Parsed dependency structure in the schema of `lib.pep621.parseDependencies`
build-systems = [ ]; # Returned by `lib.pep518.parseBuildSystems`
pyproject = null; # The unmarshaled contents of pyproject.toml
}
*/
loadRequirementsTxt =
{
# The contents of requirements.txt
requirements
}: {
dependencies = {
dependencies = map (x: x.requirement) (pip.parseRequirementsTxt requirements);
extras = { };
build-systems = [ ];
};
build-systems = [ ];
pyproject = null;
};
}

22
vendor/pyproject.nix/pypa.nix vendored Normal file
View file

@ -0,0 +1,22 @@
{ lib, ... }:
let
inherit (builtins) concatStringsSep filter split;
inherit (lib) isString toLower;
in
{
/* Normalize package name as documented in https://packaging.python.org/en/latest/specifications/name-normalization/#normalization
Type: normalizePackageName :: string -> string
Example:
# readPyproject "Friendly-Bard"
"friendly-bard"
*/
normalizePackageName =
let
concatDash = concatStringsSep "-";
splitSep = split "[-_\.]+";
in
name: toLower (concatDash (filter isString (splitSep name)));
}

147
vendor/pyproject.nix/renderers.nix vendored Normal file
View file

@ -0,0 +1,147 @@
{ lib
, pep508
, pep621
, ...
}:
let
inherit (builtins) attrValues length attrNames head foldl';
inherit (lib) optionalAttrs flatten mapAttrs' filterAttrs;
# Group licenses by their SPDX IDs for easy lookup
licensesBySpdxId = mapAttrs'
(_: license: {
name = license.spdxId;
value = license;
})
(filterAttrs (_: license: license ? spdxId) lib.licenses);
in
{
/*
Renders a project as an argument that can be passed to withPackages
Evaluates PEP-508 environment markers to select correct dependencies for the platform but does not validate version constraints.
For validation see `lib.validators`.
Type: withPackages :: AttrSet -> lambda
Example:
# withPackages (lib.project.loadPyproject { ... })
«lambda @ «string»:1:1»
*/
withPackages =
{
# Project metadata as returned by `lib.project.loadPyproject`
project
, # Python derivation
python
, # Python extras (optionals) to enable
extras ? [ ]
,
}:
let
filteredDeps = pep621.filterDependencies {
inherit (project) dependencies;
environ = pep508.mkEnviron python;
inherit extras;
};
namedDeps = pep621.getDependenciesNames filteredDeps;
flatDeps = namedDeps.dependencies ++ flatten (attrValues namedDeps.extras) ++ namedDeps.build-systems;
in
ps: map (dep: ps.${dep}) flatDeps;
/*
Renders a project as an argument that can be passed to buildPythonPackage/buildPythonApplication.
Evaluates PEP-508 environment markers to select correct dependencies for the platform but does not validate version constraints.
For validation see `lib.validators`.
Type: buildPythonPackage :: AttrSet -> AttrSet
Example:
# buildPythonPackage { project = lib.project.loadPyproject ...; python = pkgs.python3; }
{ pname = "blinker"; version = "1.3.3.7"; propagatedBuildInputs = [ ]; }
*/
buildPythonPackage =
{
# Project metadata as returned by `lib.project.loadPyproject`
project
, # Python derivation
python
, # Python extras (optionals) to enable
extras ? [ ]
, # Map a Python extras group name to a Nix attribute set.
# This is intended to be used with optionals such as test dependencies that you might
# want to add to checkInputs instead of propagatedBuildInputs
extrasAttrMappings ? { }
, # Which package format to pass to buildPythonPackage
# If the format is "wheel" PEP-518 build-systems are excluded from the build.
format ? "pyproject"
}:
let
filteredDeps = pep621.filterDependencies {
inherit (project) dependencies;
environ = pep508.mkEnviron python;
inherit extras;
};
namedDeps = pep621.getDependenciesNames filteredDeps;
inherit (project) pyproject;
meta =
let
project' = project.pyproject.project;
urls = project'.urls or { };
in
# Optional changelog
optionalAttrs (urls ? changelog)
{
inherit (urls) changelog;
} //
# Optional description
optionalAttrs (project' ? description) {
inherit (project') description;
} //
# Optional license
optionalAttrs (project'.license ? text) (
assert !(project'.license ? file); {
# From PEP-621:
# "The text key has a string value which is the license of the project whose meaning is that of the License field from the core metadata.
# These keys are mutually exclusive, so a tool MUST raise an error if the metadata specifies both keys."
# Hence the assert above.
license = licensesBySpdxId.${project'.license.text};
}
) //
# Only set mainProgram if we only have one script, otherwise it's ambigious which one is main
(
let
scriptNames = attrNames project'.scripts;
in
optionalAttrs (project' ? scripts && length scriptNames == 1) {
mainProgram = head scriptNames;
}
);
in
foldl'
(attrs: group:
let
attr = extrasAttrMappings.${group} or "propagatedBuildInputs";
in
attrs // {
${attr} = attrs.${attr} or [ ] ++ map (dep: python.pkgs.${dep}) namedDeps.extras.${group};
})
({
propagatedBuildInputs = map (dep: python.pkgs.${dep}) namedDeps.dependencies;
inherit format meta;
} // optionalAttrs (format != "wheel") {
nativeBuildInputs = map (dep: python.pkgs.${dep}) namedDeps.build-systems;
} // optionalAttrs (pyproject.project ? name) {
pname = pyproject.project.name;
}
// optionalAttrs (pyproject.project ? version) {
inherit (pyproject.project) version;
})
(attrNames namedDeps.extras);
}

11
vendor/pyproject.nix/util.nix vendored Normal file
View file

@ -0,0 +1,11 @@
# Small utilities for internal reuse, not exposed externally
{ lib }:
let
inherit (builtins) filter match split;
inherit (lib) isString;
isEmptyStr = s: isString s && match " *" s == null;
in
{
splitComma = s: if s == "" then [ ] else filter isEmptyStr (split " *, *" s);
}

71
vendor/pyproject.nix/validators.nix vendored Normal file
View file

@ -0,0 +1,71 @@
{ lib
, pep440
, pep508
, pep621
, pypa
, ...
}:
let
inherit (builtins) attrValues foldl' filter;
inherit (lib) flatten;
in
{
/*
Validates the Python package set held by Python (`python.pkgs`) against the parsed project.
Returns an attribute set where the name is the Python package derivation `pname` and the value is a list of the mismatching conditions.
Type: validateVersionConstraints :: AttrSet -> AttrSet
Example:
# validateVersionConstraints (lib.project.loadPyproject { ... })
{
resolvelib = {
# conditions as returned by `lib.pep440.parseVersionCond`
conditions = [ { op = ">="; version = { dev = null; epoch = 0; local = null; post = null; pre = null; release = [ 1 0 1 ]; }; } ];
# Version from Python package set
version = "0.5.5";
};
unearth = {
conditions = [ { op = ">="; version = { dev = null; epoch = 0; local = null; post = null; pre = null; release = [ 0 10 0 ]; }; } ];
version = "0.9.1";
};
}
*/
validateVersionConstraints =
{
# Project metadata as returned by `lib.project.loadPyproject`
project
, # Python derivation
python
, # Python extras (optionals) to enable
extras ? [ ]
,
}:
let
filteredDeps = pep621.filterDependencies {
inherit (project) dependencies;
environ = pep508.mkEnviron python;
inherit extras;
};
flatDeps = filteredDeps.dependencies ++ flatten (attrValues filteredDeps.extras) ++ filteredDeps.build-systems;
in
foldl'
(acc: dep:
let
pname = pypa.normalizePackageName dep.name;
pversion = python.pkgs.${pname}.version;
version = pep440.parseVersion python.pkgs.${pname}.version;
incompatible = filter (cond: ! pep440.comparators.${cond.op} version cond.version) dep.conditions;
in
if incompatible == [ ] then acc else acc // {
${pname} = {
version = pversion;
conditions = incompatible;
};
})
{ }
flatDeps;
}

31
vendor/update.py vendored Executable file
View file

@ -0,0 +1,31 @@
#!/usr/bin/env python3
import subprocess
import shutil
import json
import os
if __name__ == "__main__":
store_path = json.loads(
subprocess.check_output(
[
"nix-instantiate",
"--eval",
"--json",
"--expr",
'builtins.fetchGit { url = "git@github.com:adisbladis/pyproject.nix.git"; }',
]
)
)
try:
shutil.rmtree("pyproject.nix")
except FileNotFoundError:
pass
os.mkdir("pyproject.nix")
for filename in os.listdir(f"{store_path}/lib"):
if filename.startswith("test") or not filename.endswith(".nix"):
continue
shutil.copy(f"{store_path}/lib/{filename}", f"pyproject.nix/{filename}")