diff --git a/default.nix b/default.nix index 0fb9c8374..65d36f8f9 100644 --- a/default.nix +++ b/default.nix @@ -3,7 +3,7 @@ }: let - pyproject-nix = import ./vendor/pyproject.nix { inherit pkgs lib; }; + pyproject-nix = import ./vendor/pyproject.nix { inherit lib; }; poetryLib = import ./lib.nix { inherit lib pkgs pyproject-nix; inherit (pkgs) stdenv; }; inherit (poetryLib) readTOML; @@ -253,6 +253,8 @@ lib.makeScope pkgs.newScope (self: { inherit pyproject-nix; }; + inherit (final.callPackage ./fetchers { inherit pyproject-nix; }) fetchFromPypi; + __toPluginAble = toPluginAble final; } ) diff --git a/vendor/pyproject.nix/fetchers/default.nix b/fetchers/default.nix similarity index 56% rename from vendor/pyproject.nix/fetchers/default.nix rename to fetchers/default.nix index cf123b6a4..4a20d3542 100644 --- a/vendor/pyproject.nix/fetchers/default.nix +++ b/fetchers/default.nix @@ -1,15 +1,14 @@ -{ curl -, jq +{ pkgs , lib -, python3 -, runCommand , stdenvNoCC +, pyproject-nix }: let - inherit (builtins) substring filter head nixPath elemAt; + inherit (builtins) substring elemAt; inherit (lib) toLower; - pyproject = import ../lib { inherit lib; }; + inherit (pyproject-nix.lib.pypa) matchWheelFileName; + inherit (pyproject-nix.lib.eggs) matchEggFileName; # Predict URL from the PyPI index. # Args: @@ -25,8 +24,8 @@ let file }: let - matchedWheel = pyproject.pypa.matchWheelFileName file; - matchedEgg = pyproject.pypa.matchEggFileName file; + matchedWheel = matchWheelFileName file; + matchedEgg = matchEggFileName file; kind = if matchedWheel != null then "wheel" else if matchedEgg != null then elemAt matchedEgg 2 @@ -63,8 +62,8 @@ lib.mapAttrs (_: func: lib.makeOverridable func) { stdenvNoCC.mkDerivation { name = file; nativeBuildInputs = [ - curl - jq + pkgs.curl + pkgs.jq ]; isWheel = lib.strings.hasSuffix "whl" file; system = "builtin"; @@ -88,43 +87,4 @@ lib.mapAttrs (_: func: lib.makeOverridable func) { 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 - runCommand file - ({ - nativeBuildInputs = [ python3 ]; - impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ (lib.optional lib.inPureEvalMode "NETRC"); - outputHashMode = "flat"; - outputHashAlgo = "sha256"; - outputHash = hash; - passthru.isWheel = lib.strings.hasSuffix "whl" file; - } // lib.optionalAttrs (!lib.inPureEvalMode) { NETRC = netrc_file; }) '' - python ${./fetch-from-legacy.py} ${url} ${pname} ${file} - mv ${file} $out - ''; } diff --git a/vendor/pyproject.nix/fetchers/fetch-from-legacy.py b/fetchers/fetch-from-legacy.py similarity index 96% rename from vendor/pyproject.nix/fetchers/fetch-from-legacy.py rename to fetchers/fetch-from-legacy.py index 790458bb1..b39ef5df4 100644 --- a/vendor/pyproject.nix/fetchers/fetch-from-legacy.py +++ b/fetchers/fetch-from-legacy.py @@ -77,7 +77,9 @@ def handle_endtag(self, tag: str) -> None: if username and password: import base64 - password_b64 = base64.b64encode(":".join((username, password)).encode()).decode("utf-8") + 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() @@ -85,7 +87,9 @@ def handle_endtag(self, tag: str) -> None: 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)) + print( + "The file %s has not be found in the index %s" % (package_filename, index_url) + ) exit(1) package_file = open(package_filename, "wb") diff --git a/vendor/pyproject.nix/fetchers/fetch-from-pypi.sh b/fetchers/fetch-from-pypi.sh similarity index 100% rename from vendor/pyproject.nix/fetchers/fetch-from-pypi.sh rename to fetchers/fetch-from-pypi.sh diff --git a/mk-poetry-dep.nix b/mk-poetry-dep.nix index b34137cdb..6d8db64e2 100644 --- a/mk-poetry-dep.nix +++ b/mk-poetry-dep.nix @@ -6,6 +6,8 @@ , pep508Env , pyVersion , pyproject-nix +, fetchPypiLegacy +, fetchFromPypi }: { name , version @@ -224,14 +226,14 @@ pythonPackages.callPackage else if isFile then localDepPath else if isLegacy then - pyproject-nix.fetchers.fetchFromLegacy + fetchPypiLegacy { pname = name; inherit (fileInfo) file hash; inherit (source) url; } else - pyproject-nix.fetchers.fetchFromPypi { + fetchFromPypi { pname = name; inherit (fileInfo) file hash; inherit version; diff --git a/vendor/pyproject.nix/default.nix b/vendor/pyproject.nix/default.nix index e22620613..896cf9968 100644 --- a/vendor/pyproject.nix/default.nix +++ b/vendor/pyproject.nix/default.nix @@ -1,5 +1,4 @@ -{ pkgs, lib }: +{ lib }: { lib = import ./lib { inherit lib; }; - fetchers = pkgs.callPackage ./fetchers { }; } diff --git a/vendor/pyproject.nix/lib/default.nix b/vendor/pyproject.nix/lib/default.nix index 294e6a9cc..ac5c2a8f6 100644 --- a/vendor/pyproject.nix/lib/default.nix +++ b/vendor/pyproject.nix/lib/default.nix @@ -11,7 +11,7 @@ fix (self: mapAttrs (_: path: import path ({ inherit lib; } // self)) { renderers = ./renderers.nix; validators = ./validators.nix; poetry = ./poetry.nix; - + eggs = ./eggs.nix; pep440 = ./pep440.nix; pep508 = ./pep508.nix; pep518 = ./pep518.nix; diff --git a/vendor/pyproject.nix/lib/eggs.nix b/vendor/pyproject.nix/lib/eggs.nix new file mode 100644 index 000000000..15d47fe41 --- /dev/null +++ b/vendor/pyproject.nix/lib/eggs.nix @@ -0,0 +1,87 @@ +{ lib, ... }: +let + inherit (builtins) filter match elemAt compareVersions sort; + inherit (lib) isString; + + # Tag normalization documented in + # https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/#details + normalizedImpls = { + py = "python"; + cp = "cpython"; + ip = "ironpython"; + pp = "pypy"; + jy = "jython"; + }; + normalizeImpl = t: normalizedImpls.${t} or t; + +in +lib.fix (self: { + /* Regex match an egg file name, returning a list of match groups. Returns null if no match. + + Type: matchEggFileName :: string -> [ string ] + */ + matchEggFileName = name: + let + m = match "([^-]+)-([^-]+)-(.+)\\.egg" name; + in + if m != null then filter isString m else null; + + /* Check whether string is an egg file or not. + + Type: isEggFileName :: string -> bool + + Example: + # isEggFileName "cryptography-41.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + false + */ + isEggFileName = + # The filename string + name: self.matchEggFileName name != null; + + /* Parse an egg file name. + + Type: parsehEggFileName :: string -> AttrSet + + Example: + # parseEggFileName + */ + parseEggFileName = name: + let + m = self.matchEggFileName name; + mAt = elemAt m; + langM = match "([^0-9]*)(.+)" (mAt 2); + langAt = elemAt langM; + in + assert m != null; { + filename = name; + distribution = mAt 0; + version = mAt 1; + languageTag = { + implementation = normalizeImpl (langAt 0); + version = langAt 1; + }; + }; + + /* Select compatible eggs from a list and return them in priority order. + + Type: selectEggs :: derivation -> [ AttrSet ] -> [ AttrSet ] + */ + selectEggs = + # Python interpreter derivation + python: + # List of files parsed by parseEggFileName + files: + let + inherit (python.passthru) pythonVersion implementation; + + langCompatible = filter + (file: file.languageTag.implementation == "python" || file.languageTag.implementation == implementation) + files; + + versionCompatible = filter + (file: compareVersions pythonVersion file.languageTag.version >= 0) + langCompatible; + + in + sort (a: b: compareVersions a.languageTag.version b.languageTag.version > 0) versionCompatible; +}) diff --git a/vendor/pyproject.nix/lib/pep440.nix b/vendor/pyproject.nix/lib/pep440.nix index 59b695826..158c42f31 100644 --- a/vendor/pyproject.nix/lib/pep440.nix +++ b/vendor/pyproject.nix/lib/pep440.nix @@ -1,22 +1,23 @@ { lib, ... }: let - inherit (builtins) split filter match length elemAt head foldl' fromJSON typeOf; - inherit (lib) fix isString toInt toLower sublist; + inherit (builtins) split filter match length elemAt head fromJSON typeOf compareVersions; + inherit (lib) fix isString toInt sublist findFirst; inherit (import ./util.nix { inherit lib; }) splitComma; - 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: + toIntRelease = let - n = fromJSON (head (match "0?([[:digit:]]+)" s)); + matchDigit = match "0?([[:digit:]]+)"; in - assert typeOf n == "int"; n; + s: + if s == "*" then s + else + let + n = fromJSON (head (matchDigit 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; + emptyVersion = { dev = null; epoch = 0; local = null; post = null; pre = null; release = [ ]; }; # We consider some words to be alternate spellings of other words and # in those cases we want to normalize the spellings to our preferred @@ -31,37 +32,16 @@ let 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 "\\+"; + normalizedReleaseType = type: normalizedReleaseTypes.${type} or type; # Compare the release fields from the parsed version compareRelease = offset: ra: rb: - let - x = elemAt ra offset; - y = elemAt rb offset; - in if length ra == offset || length rb == offset then 0 else ( + let + x = elemAt ra offset; + y = elemAt rb offset; + in if x == "*" || y == "*" then 0 # Wildcards are always considered equal else ( @@ -117,16 +97,54 @@ fix (self: { */ parseVersion = version: let - tokens = filter isString (split "\\." version); + # Split input into (_, epoch, release, modifiers) + tokens = match "(([0-9]+)!)?([^-\+a-zA-Z]+)(.*)" version; + tokenAt = elemAt tokens; + + # Segments + epochSegment = tokenAt 1; + releaseSegment = tokenAt 2; + modifierLocalSegment = tokenAt 3; + + # Split modifier/local segment + mLocalAt = elemAt (match "([^\\+]*)\\+?(.*)" modifierLocalSegment); + modifiersSegment = mLocalAt 0; + local = mLocalAt 1; + + # Parse each post345/dev1 string into attrset + modifiers = + map + (mod: + let + # Split post345 into ["post" "345"] + m = match "-?([^0-9]+)([0-9]+)" mod; + mAt = elemAt m; + in + assert m != null; { + type = normalizedReleaseType (mAt 0); + value = toIntRelease (mAt 1); + }) + (filter (s: isString s && s != "") (split "\\." modifiersSegment)); + in - { + if version == "" then emptyVersion + else { # 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; + epoch = + if epochSegment != null then toInt epochSegment + else 0; + + # Parse release segments delimited by dots into list of ints + release = map toIntRelease (filter (s: isString s && s != "") (split "\\." releaseSegment)); + + # Find modifiers in modifiers list + pre = findFirst (mod: mod.type == "rc" || mod.type == "b" || mod.type == "a") null modifiers; + post = findFirst (mod: mod.type == "post") null modifiers; + dev = findFirst (mod: mod.type == "dev") null modifiers; + + # Local releases needs to be treated specially. + # The value isn't just a straight up number, but an arbitrary string. + local = if local != "" then local else null; }; /* Parse a version conditional. @@ -210,53 +228,47 @@ fix (self: { # 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. + compareVersions = + a: + b: + let + releaseComp = compareRelease 0 a.release b.release; + preComp = compareVersionModifier a.pre b.pre; + devComp = compareVersionModifier a.dev b.dev; + postComp = compareVersionModifier a.post b.post; + localComp = compareVersions a.local b.local; + in + if a.epoch > b.epoch then 1 + else if a.epoch < b.epoch then -1 # Compare release field - (compareRelease 0 a.release b.release) + else if releaseComp != 0 then releaseComp # 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 - ) + else if a.pre != null && b.pre != null && preComp != 0 then preComp + else if a.pre != null && b.pre == null then -1 + else if b.pre != null && a.pre == null then 1 # 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 - ) + else if a.dev != null && b.dev != null && devComp != 0 then devComp + else if a.dev != null && b.dev == null then -1 + else if b.dev != null && a.dev == null then 1 # 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 - ) + else if a.post != null && b.post != null && postComp != 0 then postComp + else if a.post != null && b.post == null then 1 + else if b.post != null && a.post == null then -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 - ) - ]; + # HACK: Local are arbitrary strings. + # We do a best estimate by comparing local as versions using builtins.compareVersions. + # This is strictly not correct but it's better than no handling.. + else if a.local != null && b.local != null && localComp != 0 then localComp + else if a.local != null && b.local == null then 1 + else if b.local != null && a.local == null then -1 + + # Equal + else 0; /* Map comparison operators as strings to a comparator function. @@ -275,17 +287,22 @@ fix (self: { 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; - }) - ); + "~=" = + let + gte = self.comparators.">="; + eq = self.comparators."=="; + in + a: b: ( + # Local version identifiers are NOT permitted in this version specifier. + assert a.local == null && b.local == null; + gte a b && eq 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; diff --git a/vendor/pyproject.nix/lib/pep508.nix b/vendor/pyproject.nix/lib/pep508.nix index ada16bab0..8203bfbc0 100644 --- a/vendor/pyproject.nix/lib/pep508.nix +++ b/vendor/pyproject.nix/lib/pep508.nix @@ -35,12 +35,15 @@ let ); # Remove groupings ( ) from expression - unparen = expr': + unparen = let - expr = stripStr expr'; - m = match "\\((.+)\\)" expr; + matchParen = match "[\t ]*\\((.+)\\)[\t ]*"; in - if m != null then elemAt m 0 else expr; + expr: + let + m = matchParen expr; + in + if m != null then head m else expr; isMarkerVariable = let @@ -62,13 +65,16 @@ let 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); + else + ( + 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 + fromJSON (if singleTicked != null then "\"" + head singleTicked + "\"" else value) + ); # Comparators for simple equality # For versions see pep440.comparators @@ -376,7 +382,20 @@ fix (self: in { - inherit (package) name conditions extras; + name = + if package.name != null then package.name + # Infer name from URL if no name was specified explicitly + else if tokens.url != null then + ( + let + inherit (tokens) url; + mEggFragment = match ".+#egg=(.+)" url; + in + if mEggFragment != null then elemAt mEggFragment 0 + else null + ) + else null; + inherit (package) conditions extras; inherit (tokens) url; markers = if tokens.markerSegment == null then null else self.parseMarkers tokens.markerSegment; }; diff --git a/vendor/pyproject.nix/lib/pep621.nix b/vendor/pyproject.nix/lib/pep621.nix index dfa078c5c..aa76bc617 100644 --- a/vendor/pyproject.nix/lib/pep621.nix +++ b/vendor/pyproject.nix/lib/pep621.nix @@ -1,10 +1,10 @@ -{ lib, pep440, pep508, pep518, ... }: +{ lib, pep440, pep508, pep518, pypa, ... }: 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); + getAttrPath = path: lib.attrByPath (splitAttrPath path) { }; in fix (self: { @@ -65,13 +65,15 @@ fix (self: { */ getDependenciesNames = let - getNames = map (dep: dep.name); + normalize = pypa.normalizePackageName; + getNames = map (dep: normalize 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 diff --git a/vendor/pyproject.nix/lib/poetry.nix b/vendor/pyproject.nix/lib/poetry.nix index a389f27d1..5562c8113 100644 --- a/vendor/pyproject.nix/lib/poetry.nix +++ b/vendor/pyproject.nix/lib/poetry.nix @@ -174,6 +174,10 @@ in inherit (poetry) keywords; } // optionalAttrs (poetry ? classifiers) { inherit (poetry) classifiers; + } // optionalAttrs (poetry ? scripts) { + inherit (poetry) scripts; + } // optionalAttrs (poetry ? plugins) { + entry-points = poetry.plugins; }; }; @@ -197,7 +201,7 @@ in */ parseDependencies = pyproject: { dependencies = map parseDependency (normalizeDependendenciesToList (pyproject.tool.poetry.dependencies or { })); - extras = mapAttrs (_: g: map parseDependency (normalizeDependendenciesToList g.dependencies)) pyproject.tool.poetry.group or { }; + extras = mapAttrs (_: group: map parseDependency (normalizeDependendenciesToList group.dependencies)) pyproject.tool.poetry.group or { }; build-systems = pep518.parseBuildSystems pyproject; }; @@ -210,7 +214,7 @@ in */ parseVersionCond = cond: ( let - m = match "^([~[:digit:]^])(.+)$" cond; + m = match "^([~^])?([a-zA-Z0-9].+)$" cond; mAt = elemAt m; c = mAt 0; rest = mAt 1; diff --git a/vendor/pyproject.nix/lib/project.nix b/vendor/pyproject.nix/lib/project.nix index e29d1aa6c..a463fa286 100644 --- a/vendor/pyproject.nix/lib/project.nix +++ b/vendor/pyproject.nix/lib/project.nix @@ -1,7 +1,16 @@ -{ pep518, pep621, poetry, pip, ... }: +{ pep621, poetry, pip, lib, renderers, validators, ... }: -{ - /* Load dependencies from a pyproject.toml. +let + inherit (builtins) mapAttrs; + + # Map over renderers and inject project argument. + # This allows for a user interface like: + # project.renderers.buildPythonPackage { } where project is already curried. + curryProject = attrs: project: lib.mapAttrs (_: func: args: func (args // { inherit project; })) attrs; + +in +lib.fix (self: { + /* Load dependencies from a PEP-621 pyproject.toml. Type: loadPyproject :: AttrSet -> AttrSet @@ -11,18 +20,56 @@ 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 + projectRoot = null; # Path to project root + requires-python = null; # requires-python as parsed by pep621.parseRequiresPython } */ loadPyproject = { # The unmarshaled contents of pyproject.toml - pyproject - # Example: extrasAttrPaths = [ "tool.pdm.dev-dependencies" ]; - , extrasAttrPaths ? [ ] - }: { + pyproject ? lib.importTOML (projectRoot + "/pyproject.toml") + , # Example: extrasAttrPaths = [ "tool.pdm.dev-dependencies" ]; + extrasAttrPaths ? [ ] + , # Path to project root + projectRoot ? null + , + }: lib.fix (project: { dependencies = pep621.parseDependencies { inherit pyproject extrasAttrPaths; }; - build-systems = pep518.parseBuildSystems pyproject; - inherit pyproject; + inherit pyproject projectRoot; + renderers = curryProject renderers project; + validators = curryProject validators project; + requires-python = pep621.parseRequiresPython pyproject; + }); + + /* Load dependencies from a PDM pyproject.toml. + + Type: loadPDMPyproject :: 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 + projectRoot = null; # Path to project root + requires-python = null; # requires-python as parsed by pep621.parseRequiresPython + } + */ + loadPDMPyproject = + { + # The unmarshaled contents of pyproject.toml + pyproject ? lib.importTOML (projectRoot + "/pyproject.toml") + , # Path to project root + projectRoot ? null + , # The unmarshaled contents of pdm.lock + pdmLock ? lib.importTOML (projectRoot + "/pdm.lock") + , + }: self.loadPyproject + { + inherit pyproject projectRoot; + extrasAttrPaths = [ "tool.pdm.dev-dependencies" ]; + } // { + inherit pdmLock; }; /* Load dependencies from a Poetry pyproject.toml. @@ -35,22 +82,32 @@ 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 + projectRoot = null; # Path to project root + requires-python = null; # requires-python as parsed by pep621.parseRequiresPython } */ loadPoetryPyproject = { # The unmarshaled contents of pyproject.toml - pyproject + pyproject ? lib.importTOML (projectRoot + "/pyproject.toml") + , # Path to project root + projectRoot ? null + , # The unmarshaled contents of pyproject.toml + poetryLock ? lib.importTOML (projectRoot + "/poetry.lock") + , }: let pyproject-pep621 = poetry.translatePoetryProject pyproject; in - { + lib.fix (project: { dependencies = poetry.parseDependencies pyproject; - build-systems = pep518.parseBuildSystems pyproject; pyproject = pyproject-pep621; pyproject-poetry = pyproject; - }; + renderers = curryProject renderers project; + validators = curryProject validators project; + inherit projectRoot poetryLock; + requires-python = null; + }); /* Load dependencies from a requirements.txt. @@ -64,19 +121,58 @@ 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 + projectRoot = null; # Path to project root + requires-python = null; # requires-python as parsed by pep621.parseRequiresPython } */ loadRequirementsTxt = { # The contents of requirements.txt - requirements - }: { + requirements ? builtins.readFile (projectRoot + "/requirements.txt") + , # Path to project root + projectRoot ? null + , + }: lib.fix (project: { dependencies = { dependencies = map (x: x.requirement) (pip.parseRequirementsTxt requirements); extras = { }; build-systems = [ ]; }; - build-systems = [ ]; pyproject = null; + renderers = curryProject renderers project; + validators = curryProject validators project; + inherit projectRoot; + requires-python = null; + }); + + /* Load dependencies from a either a PEP-621 or Poetry pyproject.toml file. + This function is intended for 2nix authors that wants to include local pyproject.toml files + but don't know up front whether they're from Poetry or PEP-621. + + Type: loadPyprojectDynamic :: AttrSet -> AttrSet + + Example: + # loadPyprojectDynamic { 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 + projectRoot = null; # Path to project root + requires-python = null; # requires-python as parsed by pep621.parseRequiresPython + } + */ + loadPyprojectDynamic = + { + # The unmarshaled contents of pyproject.toml + pyproject ? lib.importTOML (projectRoot + "/pyproject.toml") + , # Path to project root + projectRoot ? null + }: + let + isPoetry = lib.hasAttrByPath [ "tool" "poetry" ] pyproject; + isPep621 = lib.hasAttrByPath [ "project" ] pyproject; + in + (if isPoetry then self.loadPoetryPyproject else if isPep621 then self.loadPyproject else throw "Project is neither Poetry nor PEP-621") { + inherit pyproject projectRoot; }; -} +}) diff --git a/vendor/pyproject.nix/lib/pypa.nix b/vendor/pyproject.nix/lib/pypa.nix index 204d1c78e..99f308817 100644 --- a/vendor/pyproject.nix/lib/pypa.nix +++ b/vendor/pyproject.nix/lib/pypa.nix @@ -118,16 +118,6 @@ lib.fix (self: { in if m != null then filter isString m else null; - /* Regex match an egg file name, returning a list of match groups. Returns null if no match. - - Type: matchEggFileName :: string -> [ string ] - */ - matchEggFileName = name: - let - m = match "([^-]+)-([^-]+)-(.+)\\.egg" name; - in - if m != null then filter isString m else null; - /* Check whether string is a wheel file or not. Type: isWheelFileName :: string -> bool @@ -332,10 +322,10 @@ lib.fix (self: { /* Select compatible wheels from a list and return them in priority order. - Type: selectWheels :: derivation -> [ AttrSet ] -> [ AttrSet ] + Type: selectWheels :: AttrSet -> derivation -> [ AttrSet ] -> [ AttrSet ] Example: - # selectWheels pkgs.python3 [ (pypa.parseWheelFileName "Pillow-9.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl") ] + # selectWheels (lib.systems.elaborate "x86_64-linux") [ (pypa.parseWheelFileName "Pillow-9.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl") ] [ (pypa.parseWheelFileName "Pillow-9.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl") ] */ selectWheels = @@ -345,43 +335,52 @@ lib.fix (self: { python: # List of files as parsed by parseWheelFileName files: - let - # Get sorting/filter criteria fields - withSortedTags = map - (file: - let - abiCompatible = self.isABITagCompatible python file.abiTag; - - # Filter only compatible tags - languageTags = filter (self.isPythonTagCompatible python) file.languageTags; - # Extract the tag as a number. E.g. "37" is `toInt "37"` and "none"/"any" is 0 - languageTags' = map (tag: if tag == "none" then 0 else toInt tag.version) languageTags; - - in - { - bestLanguageTag = head (sort (x: y: x > y) languageTags'); - compatible = abiCompatible && length languageTags > 0 && lib.any (self.isPlatformTagCompatible platform python.stdenv.cc.libc) file.platformTags; - inherit file; - }) - files; - - # Only consider files compatible with this interpreter - compatibleFiles = filter (file: file.compatible) withSortedTags; - - # Sort files based on their tags - sorted = sort - ( - x: y: - x.file.distribution > y.file.distribution - || x.file.version > y.file.version - || (x.file.buildTag != null && (y.file.buildTag == null || x.file.buildTag > y.file.buildTag)) - || x.bestLanguageTag > y.bestLanguageTag - ) - compatibleFiles; - - in - # Strip away temporary sorting metadata - map (file': file'.file) sorted + if ! lib.isAttrs platform + then + throw '' + SelectWheel was called with wrong type for its first argument 'platform'. + Pass only elaborated platforms. + Example: + `lib.systems.elaborate "x86_64-linux"` + '' + else + let + # Get sorting/filter criteria fields + withSortedTags = map + (file: + let + abiCompatible = self.isABITagCompatible python file.abiTag; + + # Filter only compatible tags + languageTags = filter (self.isPythonTagCompatible python) file.languageTags; + # Extract the tag as a number. E.g. "37" is `toInt "37"` and "none"/"any" is 0 + languageTags' = map (tag: if tag == "none" then 0 else toInt tag.version) languageTags; + + in + { + bestLanguageTag = head (sort (x: y: x > y) languageTags'); + compatible = abiCompatible && length languageTags > 0 && lib.any (self.isPlatformTagCompatible platform python.stdenv.cc.libc) file.platformTags; + inherit file; + }) + files; + + # Only consider files compatible with this interpreter + compatibleFiles = filter (file: file.compatible) withSortedTags; + + # Sort files based on their tags + sorted = sort + ( + x: y: + x.file.distribution > y.file.distribution + || x.file.version > y.file.version + || (x.file.buildTag != null && (y.file.buildTag == null || x.file.buildTag > y.file.buildTag)) + || x.bestLanguageTag > y.bestLanguageTag + ) + compatibleFiles; + + in + # Strip away temporary sorting metadata + map (file': file'.file) sorted ; }) diff --git a/vendor/pyproject.nix/lib/renderers.nix b/vendor/pyproject.nix/lib/renderers.nix index 870778656..1199ceaed 100644 --- a/vendor/pyproject.nix/lib/renderers.nix +++ b/vendor/pyproject.nix/lib/renderers.nix @@ -1,5 +1,6 @@ { lib , pep508 +, pep440 , pep621 , ... }: @@ -37,7 +38,8 @@ in python , # Python extras (optionals) to enable extras ? [ ] - , + , # Extra withPackages function + extraPackages ? _ps: [ ] }: let filteredDeps = pep621.filterDependencies { @@ -48,7 +50,13 @@ in namedDeps = pep621.getDependenciesNames filteredDeps; flatDeps = namedDeps.dependencies ++ flatten (attrValues namedDeps.extras) ++ namedDeps.build-systems; in - ps: map (dep: ps.${dep}) flatDeps; + ps: + let + buildSystems' = + if namedDeps.build-systems != [ ] then [ ] + else [ ps.setuptools ps.wheel ]; + in + map (dep: ps.${dep}) flatDeps ++ extraPackages ps ++ buildSystems'; /* Renders a project as an argument that can be passed to buildPythonPackage/buildPythonApplication. @@ -70,7 +78,8 @@ in python , # Python extras (optionals) to enable extras ? [ ] - , # Map a Python extras group name to a Nix attribute set. + , # Map a Python extras group name to a Nix attribute set like: + # { dev = "checkInputs"; } # This is intended to be used with optionals such as test dependencies that you might # want to add to checkInputs instead of propagatedBuildInputs extrasAttrMappings ? { } @@ -85,6 +94,10 @@ in inherit extras; }; + pythonVersion = pep440.parseVersion python.version; + + pythonPackages = python.pkgs; + namedDeps = pep621.getDependenciesNames filteredDeps; inherit (project) pyproject; @@ -104,13 +117,13 @@ in inherit (project') description; } // # Optional license - optionalAttrs (project'.license ? text) ( + optionalAttrs (lib.hasAttrByPath [ "license" "text" ] project') ( 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}; + license = licensesBySpdxId.${project'.license.text} or project'.license.text; } ) // # Only set mainProgram if we only have one script, otherwise it's ambigious which one is main @@ -135,13 +148,23 @@ in ({ propagatedBuildInputs = map (dep: python.pkgs.${dep}) namedDeps.dependencies; inherit format meta; + passthru = { + optional-dependencies = lib.mapAttrs (_group: deps: map (dep: python.pkgs.${dep.name}) deps) project.dependencies.extras; + }; } // optionalAttrs (format != "wheel") { - nativeBuildInputs = map (dep: python.pkgs.${dep}) namedDeps.build-systems; + nativeBuildInputs = + if namedDeps.build-systems != [ ] then map (dep: pythonPackages.${dep}) namedDeps.build-systems + else [ pythonPackages.setuptools pythonPackages.wheel ]; } // optionalAttrs (pyproject.project ? name) { pname = pyproject.project.name; + } // optionalAttrs (project.projectRoot != null) { + src = project.projectRoot; } // optionalAttrs (pyproject.project ? version) { inherit (pyproject.project) version; + } + // optionalAttrs (project.requires-python != null) { + disabled = ! lib.all (spec: pep440.comparators.${spec.op} pythonVersion spec.version) project.requires-python; }) (attrNames namedDeps.extras); } diff --git a/vendor/update.py b/vendor/update.py index c498351f9..1196cf513 100755 --- a/vendor/update.py +++ b/vendor/update.py @@ -35,4 +35,4 @@ continue shutil.copy(f"{store_path}/lib/{filename}", f"pyproject.nix/lib/{filename}") - shutil.copytree(f"{store_path}/fetchers", "pyproject.nix/fetchers") + # shutil.copytree(f"{store_path}/fetchers", "pyproject.nix/fetchers")