vendor: bump pyproject.nix

This commit is contained in:
adisbladis 2023-10-26 16:39:13 +13:00
parent 40192db77c
commit 15185adb4c
23 changed files with 396 additions and 97 deletions

View file

@ -5,10 +5,10 @@
let
inherit (poetryLib) isCompatible readTOML;
pyproject-nix = import ./vendor/pyproject.nix { inherit lib; };
pyproject-nix = import ./vendor/pyproject.nix { inherit pkgs lib; };
# Name normalization
inherit (pyproject-nix.pypa) normalizePackageName;
inherit (pyproject-nix.lib.pypa) normalizePackageName;
normalizePackageSet = lib.attrsets.mapAttrs' (name: value: lib.attrsets.nameValuePair (normalizePackageName name) value);
# Map SPDX identifiers to license names
@ -172,7 +172,7 @@ lib.makeScope pkgs.newScope (self: {
in
lib.listToAttrs (lib.mapAttrsToList (n: v: { name = normalizePackageName n; value = v; }) lockfiles);
pep508Env = pyproject-nix.pep508.mkEnviron python;
pep508Env = pyproject-nix.lib.pep508.mkEnviron python;
# Filter packages by their PEP508 markers & pyproject interpreter version
partitions =
@ -181,9 +181,9 @@ lib.makeScope pkgs.newScope (self: {
if pkgMeta ? marker then
(
let
marker = pyproject-nix.pep508.parseMarkers pkgMeta.marker;
marker = pyproject-nix.lib.pep508.parseMarkers pkgMeta.marker;
in
pyproject-nix.pep508.evalMarkers pep508Env marker
pyproject-nix.lib.pep508.evalMarkers pep508Env marker
) else true && isCompatible (poetryLib.getPythonVersion python) pkgMeta.python-versions;
in
lib.partition supportsPythonVersion poetryLock.package;

View file

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

View file

@ -29,7 +29,7 @@ pythonPackages.callPackage
}@args:
let
inherit (python) stdenv;
inherit (pyproject-nix.pypa) normalizePackageName;
inherit (pyproject-nix.lib.pypa) normalizePackageName;
inherit (poetryLib) isCompatible getManyLinuxDeps fetchFromLegacy fetchFromPypi;
inherit (import ./pep425.nix {
@ -145,7 +145,7 @@ pythonPackages.callPackage
pep508Markers = v.markers or "";
in
compat constraints && (if pep508Markers == "" then true else
(pyproject-nix.pep508.evalMarkers
(pyproject-nix.lib.pep508.evalMarkers
(pep508Env // {
extra = {
# All extras are always enabled
@ -153,7 +153,7 @@ pythonPackages.callPackage
value = lib.attrNames extras;
};
})
(pyproject-nix.pep508.parseMarkers pep508Markers)))
(pyproject-nix.lib.pep508.parseMarkers pep508Markers)))
)
dependencies
);

View file

@ -1,7 +1,7 @@
{ lib, stdenv, python, pyproject-nix, isLinux ? stdenv.isLinux }:
let
inherit (lib.strings) escapeRegex hasPrefix hasSuffix hasInfix splitString removeSuffix;
targetMachine = pyproject-nix.pep599.manyLinuxTargetMachines.${stdenv.targetPlatform.parsed.cpu.name};
targetMachine = pyproject-nix.lib.pep599.manyLinuxTargetMachines.${stdenv.targetPlatform.parsed.cpu.name};
pythonVer =
let

View file

@ -1,22 +1,5 @@
{ 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;
})
{ pkgs, lib }:
{
lib = import ./lib { inherit lib; };
fetchers = import ./fetchers { inherit pkgs lib; };
}

View file

@ -0,0 +1,122 @@
{ pkgs
, lib
,
}:
let
inherit (builtins) substring filter head nixPath;
inherit (lib) toLower;
# Predict URL from the PyPI index.
# Args:
# pname: package name
# file: filename including extension
# hash: SRI hash
# kind: Language implementation and version tag
predictURLFromPypi =
{
# package name
pname
, # filename including extension
file
, # Language implementation and version tag
kind
,
}: "https://files.pythonhosted.org/packages/${kind}/${toLower (substring 0 1 file)}/${pname}/${file}";
in
lib.mapAttrs (_: func: lib.makeOverridable func) {
/*
Fetch from the PyPI index.
At first we try to fetch the predicated URL but if that fails we
will use the Pypi API to determine the correct URL.
Type: fetchFromPypi :: AttrSet -> derivation
*/
fetchFromPypi =
{
# package name
pname
, # filename including extension
file
, # the version string of the dependency
version
, # SRI hash
hash
, # Language implementation and version tag
kind
, # Options to pass to `curl`
curlOpts ? ""
,
}:
let
predictedURL = predictURLFromPypi { inherit pname file kind; };
in
pkgs.stdenvNoCC.mkDerivation {
name = file;
nativeBuildInputs = [
pkgs.buildPackages.curl
pkgs.buildPackages.jq
];
isWheel = lib.strings.hasSuffix "whl" file;
system = "builtin";
preferLocalBuild = true;
impureEnvVars =
lib.fetchers.proxyImpureEnvVars
++ [
"NIX_CURL_FLAGS"
];
inherit pname file version curlOpts predictedURL;
builder = ./fetch-from-pypi.sh;
outputHashMode = "flat";
outputHashAlgo = "sha256";
outputHash = hash;
passthru = {
urls = [ predictedURL ]; # retain compatibility with nixpkgs' fetchurl
};
};
/*
Fetch from the PyPI legacy API.
Some repositories (such as Devpi) expose the Pypi legacy API (https://warehouse.pypa.io/api-reference/legacy.html).
Type: fetchFromLegacy :: AttrSet -> derivation
*/
fetchFromLegacy =
{
# package name
pname
, # URL to package index
url
, # filename including extension
file
, # SRI hash
hash
,
}:
let
pathParts = filter ({ prefix, path }: "NETRC" == prefix) nixPath; # deadnix: skip
netrc_file =
if (pathParts != [ ])
then (head pathParts).path
else "";
in
pkgs.runCommand file
{
nativeBuildInputs = [ pkgs.buildPackages.python3 ];
impureEnvVars = lib.fetchers.proxyImpureEnvVars;
outputHashMode = "flat";
outputHashAlgo = "sha256";
outputHash = hash;
NETRC = netrc_file;
passthru.isWheel = lib.strings.hasSuffix "whl" file;
} ''
python ${./fetch-from-legacy.py} ${url} ${pname} ${file}
mv ${file} $out
'';
}

View file

@ -0,0 +1,137 @@
# Some repositories (such as Devpi) expose the Pypi legacy API
# (https://warehouse.pypa.io/api-reference/legacy.html).
#
# Note it is not possible to use pip
# https://discuss.python.org/t/pip-download-just-the-source-packages-no-building-no-metadata-etc/4651/12
import netrc
import os
import shutil
import ssl
import sys
import urllib.request
from html.parser import HTMLParser
from os.path import normpath
from typing import Optional
from urllib.parse import urlparse, urlunparse
# Parse the legacy index page to extract the href and package names
class Pep503(HTMLParser):
def __init__(self) -> None:
super().__init__()
self.sources: dict[str, str] = {}
self.url: Optional[str] = None
self.name: Optional[str] = None
def handle_data(self, data: str) -> None:
if self.url is not None:
self.name = data
def handle_starttag(self, tag: str, attrs: list[tuple[str, Optional[str]]]) -> None:
if tag == "a":
for name, value in attrs:
if name == "href":
self.url = value
def handle_endtag(self, tag: str) -> None:
if self.url is not None:
if not self.name:
raise ValueError("Name not set")
self.sources[self.name] = self.url
self.url = None
url = sys.argv[1]
package_name = sys.argv[2]
index_url = url + "/" + package_name + "/"
package_filename = sys.argv[3]
# Parse username and password for this host from the netrc file if given.
username: Optional[str] = None
password: Optional[str] = None
if os.environ["NETRC"]:
netrc_obj = netrc.netrc(os.environ["NETRC"])
host = urlparse(index_url).netloc
# Strip port number if present
if ":" in host:
host = host.split(":")[0]
authenticators = netrc_obj.authenticators(host)
if authenticators:
username, _, password = authenticators
print("Reading index %s" % index_url)
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
# Extract out username/password from index_url, if present.
parsed_url = urlparse(index_url)
username = parsed_url.username or username
password = parsed_url.password or password
index_url = parsed_url._replace(netloc=parsed_url.netloc.rpartition("@")[-1]).geturl()
req = urllib.request.Request(index_url)
if username and password:
import base64
password_b64 = base64.b64encode(":".join((username, password)).encode()).decode("utf-8")
req.add_header("Authorization", "Basic {}".format(password_b64))
response = urllib.request.urlopen(req, context=context)
index = response.read()
parser = Pep503()
parser.feed(str(index, "utf-8"))
if package_filename not in parser.sources:
print("The file %s has not be found in the index %s" % (package_filename, index_url))
exit(1)
package_file = open(package_filename, "wb")
# Sometimes the href is a relative or absolute path within the index's domain.
indicated_url = urlparse(parser.sources[package_filename])
if indicated_url.netloc == "":
parsed_url = urlparse(index_url)
if indicated_url.path.startswith("/"):
# An absolute path within the index's domain.
path = parser.sources[package_filename]
else:
# A relative path.
path = parsed_url.path + "/" + parser.sources[package_filename]
package_url = urlunparse(
(
parsed_url.scheme,
parsed_url.netloc,
path,
None,
None,
None,
)
)
else:
package_url = parser.sources[package_filename]
# Handle urls containing "../"
parsed_url = urlparse(package_url)
real_package_url = urlunparse(
(
parsed_url.scheme,
parsed_url.netloc,
normpath(parsed_url.path),
parsed_url.params,
parsed_url.query,
parsed_url.fragment,
)
)
print("Downloading %s" % real_package_url)
req = urllib.request.Request(real_package_url)
if username and password:
req.add_unredirected_header("Authorization", "Basic {}".format(password_b64))
response = urllib.request.urlopen(req, context=context)
with response as r:
shutil.copyfileobj(r, package_file)

View file

@ -0,0 +1,27 @@
#!/usr/bin/env bash
# shellcheck disable=SC1091,SC2154
source "$stdenv/setup"
set -euo pipefail
curl="curl \
--location \
--max-redirs 20 \
--retry 2 \
--disable-epsv \
--cookie-jar cookies \
--insecure \
--speed-time 5 \
--progress-bar \
--fail \
$curlOpts \
$NIX_CURL_FLAGS"
echo "Trying to fetch with predicted URL: $predictedURL"
$curl "$predictedURL" --output "$out" && exit 0
echo "Predicted URL '$predictedURL' failed, querying pypi.org"
$curl "https://pypi.org/pypi/$pname/json" | jq -r ".releases.\"$version\"[] | select(.filename == \"$file\") | .url" > url
url=$(cat url)
$curl "$url" --output "$out"

22
vendor/pyproject.nix/lib/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;
})

View file

@ -151,7 +151,7 @@ fix (self: {
*/
parseVersionCond = cond: (
let
m = match " *([=><!~^]+) *(.+)" cond;
m = match " *([=><!~^]*) *(.+)" cond;
mAt = elemAt m;
in
{
@ -256,6 +256,7 @@ fix (self: {
"<" = a: b: self.compareVersions a b < 0;
">" = a: b: self.compareVersions a b > 0;
"===" = throw "Arbitrary equality clause not supported";
"" = _a: _b: true;
};
})

View file

@ -3,7 +3,7 @@
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;
inherit (import ./util.nix { inherit lib; }) splitComma stripStr;
re = {
operators = "([=><!~^]+)";
@ -34,9 +34,6 @@ let
}
);
# 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

View file

@ -4,14 +4,7 @@
}:
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;
inherit (import ./util.nix { inherit lib; }) stripStr;
uncomment = l: head (match " *([^#]*).*" l);

View file

@ -4,8 +4,10 @@
, pep518
, ...
}:
lib.fix (self:
let
inherit (builtins) match elemAt foldl' typeOf attrNames head tail mapAttrs;
inherit (builtins) match elemAt foldl' typeOf attrNames head tail mapAttrs length filter split;
inherit (lib) optionalAttrs flatten;
inherit (import ./util.nix { inherit lib; }) splitComma;
@ -84,55 +86,8 @@ let
)) [ ]
(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));
parseVersionConds = s: flatten (map self.parseVersionCond (splitComma s));
dummyMarker = {
type = "bool";
@ -240,10 +195,64 @@ in
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;
};
}
/* 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;
}]
);
})

View file

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