mirror of
https://git.savannah.gnu.org/git/guix.git
synced 2025-01-18 13:36:36 +01:00
import: pypi: Support extracting dependencies from pyproject.toml.
* guix/import/pypi.scm (guess-requirements): Support extracting dependencies from pyproject.toml. * tests/pypi.scm: ("pypi->guix-package, no requires.txt, but wheel."): Renamed from "pypi->guix-package, wheels", remove requires.txt file, because the current implementation cannot detect invalid files. ("pypi->guix-package, no usable requirement file, no wheel."): Renamed from "pypi->guix-package, no usable requirement file.". (test-pyproject.toml): New variable. ("pypi->guix-package, no wheel, no requires.txt, but pyproject.toml"): New test. ("pypi->guix-package, no wheel, but requires.txt and pyproject.toml"): Ditto. Change-Id: Ib525750eb6ff4139a8209420042b28ae3c850764 Reviewed-by: Ludovic Courtès <ludo@gnu.org> Signed-off-by: Sharlatan Hellseher <sharlatanus@gmail.com>
This commit is contained in:
parent
f2b7e8f762
commit
8bb3bb19c2
2 changed files with 152 additions and 23 deletions
|
@ -57,6 +57,7 @@ (define-module (guix import pypi)
|
|||
#:use-module (guix import utils)
|
||||
#:use-module (guix import json)
|
||||
#:use-module (json)
|
||||
#:use-module (guix build toml)
|
||||
#:use-module (guix packages)
|
||||
#:use-module (guix upstream)
|
||||
#:use-module ((guix licenses) #:prefix license:)
|
||||
|
@ -386,7 +387,42 @@ (define (guess-requirements-from-wheel)
|
|||
(if wheel-url
|
||||
(and (url-fetch wheel-url temp)
|
||||
(read-wheel-metadata temp))
|
||||
#f))))
|
||||
(list '() '())))))
|
||||
|
||||
(define (guess-requirements-from-pyproject.toml dir)
|
||||
(let* ((pyproject.toml-files (find-files dir (lambda (abs-file-name _)
|
||||
(string-match "/pyproject.toml$"
|
||||
abs-file-name))))
|
||||
(pyproject.toml (match pyproject.toml-files
|
||||
(()
|
||||
(warning (G_ "Cannot guess requirements from \
|
||||
pyproject.toml file, because it does not exist.~%"))
|
||||
'())
|
||||
(else (parse-toml-file (first pyproject.toml-files)))))
|
||||
(pyproject-build-requirements
|
||||
(or (recursive-assoc-ref pyproject.toml '("build-system" "requires")) '()))
|
||||
(pyproject-dependencies
|
||||
(or (recursive-assoc-ref pyproject.toml '("project" "dependencies")) '()))
|
||||
;; This is more of a convention, since optional-dependencies is a table of arbitrary values.
|
||||
(pyproject-test-dependencies
|
||||
(or (recursive-assoc-ref pyproject.toml '("project" "optional-dependencies" "test")) '())))
|
||||
(if (null? pyproject.toml)
|
||||
#f
|
||||
(list (map specification->requirement-name pyproject-dependencies)
|
||||
(map specification->requirement-name
|
||||
(append pyproject-build-requirements
|
||||
pyproject-test-dependencies))))))
|
||||
|
||||
(define (guess-requirements-from-requires.txt dir)
|
||||
(let ((requires.txt-files (find-files dir (lambda (abs-file-name _)
|
||||
(string-match "\\.egg-info/requires.txt$"
|
||||
abs-file-name)))))
|
||||
(match requires.txt-files
|
||||
(()
|
||||
(warning (G_ "Cannot guess requirements from source archive: \
|
||||
no requires.txt file found.~%"))
|
||||
#f)
|
||||
(else (parse-requires.txt (first requires.txt-files))))))
|
||||
|
||||
(define (guess-requirements-from-source)
|
||||
;; Return the package's requirements by guessing them from the source.
|
||||
|
@ -398,27 +434,29 @@ (define (guess-requirements-from-source)
|
|||
(if (string=? "zip" (file-extension source-url))
|
||||
(invoke "unzip" archive "-d" dir)
|
||||
(invoke "tar" "xf" archive "-C" dir)))
|
||||
(let ((requires.txt-files
|
||||
(find-files dir (lambda (abs-file-name _)
|
||||
(string-match "\\.egg-info/requires.txt$"
|
||||
abs-file-name)))))
|
||||
(match requires.txt-files
|
||||
(()
|
||||
(warning (G_ "Cannot guess requirements from source archive:\
|
||||
no requires.txt file found.~%"))
|
||||
(list '() '()))
|
||||
(else (parse-requires.txt (first requires.txt-files)))))))
|
||||
(list (guess-requirements-from-pyproject.toml dir)
|
||||
(guess-requirements-from-requires.txt dir))))
|
||||
(begin
|
||||
(warning (G_ "Unsupported archive format; \
|
||||
cannot determine package dependencies from source archive: ~a~%")
|
||||
(basename source-url))
|
||||
(list '() '()))))
|
||||
(list #f #f))))
|
||||
|
||||
;; First, try to compute the requirements using the wheel, else, fallback to
|
||||
;; reading the "requires.txt" from the egg-info directory from the source
|
||||
;; archive.
|
||||
(or (guess-requirements-from-wheel)
|
||||
(guess-requirements-from-source)))
|
||||
(define (merge a b)
|
||||
"Given lists A and B with two iteams each, combine A1 and B1, as well as A2 and B2."
|
||||
(match (list a b)
|
||||
(((first-propagated first-native) (second-propagated second-native))
|
||||
(list (append first-propagated second-propagated) (append first-native second-native)))))
|
||||
|
||||
;; requires.txt and the metadata of a wheel contain redundant information,
|
||||
;; so fetch only one of them, preferring requires.txt from the source
|
||||
;; distribution, which we always fetch, since the source tarball also
|
||||
;; contains pyproject.toml.
|
||||
(match (guess-requirements-from-source)
|
||||
((from-pyproject.toml #f)
|
||||
(merge (or from-pyproject.toml '(() ())) (or (guess-requirements-from-wheel) '(() ()))))
|
||||
((from-pyproject.toml from-requires.txt)
|
||||
(merge (or from-pyproject.toml '(() ())) from-requires.txt))))
|
||||
|
||||
(define (compute-inputs source-url wheel-url archive)
|
||||
"Given the SOURCE-URL and WHEEL-URL of an already downloaded ARCHIVE, return
|
||||
|
|
103
tests/pypi.scm
103
tests/pypi.scm
|
@ -112,6 +112,20 @@ (define test-requires.txt-beaker "\
|
|||
coverage
|
||||
")
|
||||
|
||||
(define test-pyproject.toml "\
|
||||
[build-system]
|
||||
requires = [\"dummy-build-dep-a\", \"dummy-build-dep-b\"]
|
||||
|
||||
[project]
|
||||
dependencies = [
|
||||
\"dummy-dep-a\",
|
||||
\"dummy-dep-b\",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
test = [\"dummy-test-dep-a\", \"dummy-test-dep-b\"]
|
||||
")
|
||||
|
||||
(define test-metadata "\
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Requires-Dist: baz ~= 3
|
||||
|
@ -325,13 +339,90 @@ (define-syntax-rule (with-pypi responses body ...)
|
|||
(x
|
||||
(pk 'fail x #f))))))
|
||||
|
||||
(test-skip (if (which "zip") 0 1))
|
||||
(test-assert "pypi->guix-package, wheels"
|
||||
(test-assert "pypi->guix-package, no wheel, no requires.txt, but pyproject.toml"
|
||||
(let ((tarball (pypi-tarball
|
||||
"foo-1.0.0"
|
||||
'(("foo-1.0.0/foo.egg-info/requires.txt"
|
||||
"wrong data \
|
||||
to make sure we're testing wheels"))))
|
||||
`(("pyproject.toml" ,test-pyproject.toml))))
|
||||
(twice (lambda (lst) (append lst lst))))
|
||||
(with-pypi (twice `(("/foo-1.0.0.tar.gz" 200 ,(file-dump tarball))
|
||||
("/foo-1.0.0-py2.py3-none-any.whl" 404 "")
|
||||
("/foo/json" 200 ,(lambda (port)
|
||||
(display (foo-json) port)))))
|
||||
;; Not clearing the memoization cache here would mean returning the value
|
||||
;; computed in the previous test.
|
||||
(invalidate-memoization! pypi->guix-package)
|
||||
(match (pypi->guix-package "foo")
|
||||
(`(package
|
||||
(name "python-foo")
|
||||
(version "1.0.0")
|
||||
(source (origin
|
||||
(method url-fetch)
|
||||
(uri (pypi-uri "foo" version))
|
||||
(sha256
|
||||
(base32 ,(? string? hash)))))
|
||||
(build-system pyproject-build-system)
|
||||
(propagated-inputs (list python-dummy-dep-a python-dummy-dep-b))
|
||||
(native-inputs (list python-dummy-build-dep-a python-dummy-build-dep-b
|
||||
python-dummy-test-dep-a python-dummy-test-dep-b))
|
||||
(home-page "http://example.com")
|
||||
(synopsis "summary")
|
||||
(description "summary.")
|
||||
(license license:lgpl2.0))
|
||||
(and (string=? default-sha256/base32 hash)
|
||||
(equal? (pypi->guix-package "foo" #:version "1.0.0")
|
||||
(pypi->guix-package "foo"))
|
||||
(guard (c ((error? c) #t))
|
||||
(pypi->guix-package "foo" #:version "42"))))
|
||||
(x
|
||||
(pk 'fail x #f))))))
|
||||
|
||||
(test-assert "pypi->guix-package, no wheel, but requires.txt and pyproject.toml"
|
||||
(let ((tarball (pypi-tarball
|
||||
"foo-1.0.0"
|
||||
`(("foo-1.0.0/pyproject.toml" ,test-pyproject.toml)
|
||||
("foo-1.0.0/bizarre.egg-info/requires.txt"
|
||||
,test-requires.txt))))
|
||||
(twice (lambda (lst) (append lst lst))))
|
||||
(with-pypi (twice `(("/foo-1.0.0.tar.gz" 200 ,(file-dump tarball))
|
||||
("/foo-1.0.0-py2.py3-none-any.whl" 404 "")
|
||||
("/foo/json" 200 ,(lambda (port)
|
||||
(display (foo-json) port)))))
|
||||
;; Not clearing the memoization cache here would mean returning the value
|
||||
;; computed in the previous test.
|
||||
(invalidate-memoization! pypi->guix-package)
|
||||
(match (pypi->guix-package "foo")
|
||||
(`(package
|
||||
(name "python-foo")
|
||||
(version "1.0.0")
|
||||
(source (origin
|
||||
(method url-fetch)
|
||||
(uri (pypi-uri "foo" version))
|
||||
(sha256
|
||||
(base32 ,(? string? hash)))))
|
||||
(build-system pyproject-build-system)
|
||||
;; Information from requires.txt and pyproject.toml is combined.
|
||||
(propagated-inputs (list python-bar python-dummy-dep-a python-dummy-dep-b
|
||||
python-foo))
|
||||
(native-inputs (list python-dummy-build-dep-a python-dummy-build-dep-b
|
||||
python-dummy-test-dep-a python-dummy-test-dep-b
|
||||
python-pytest))
|
||||
(home-page "http://example.com")
|
||||
(synopsis "summary")
|
||||
(description "summary.")
|
||||
(license license:lgpl2.0))
|
||||
(and (string=? default-sha256/base32 hash)
|
||||
(equal? (pypi->guix-package "foo" #:version "1.0.0")
|
||||
(pypi->guix-package "foo"))
|
||||
(guard (c ((error? c) #t))
|
||||
(pypi->guix-package "foo" #:version "42"))))
|
||||
(x
|
||||
(pk 'fail x #f))))))
|
||||
|
||||
(test-skip (if (which "zip") 0 1))
|
||||
(test-assert "pypi->guix-package, no requires.txt, but wheel."
|
||||
(let ((tarball (pypi-tarball
|
||||
"foo-1.0.0"
|
||||
'(("foo-1.0.0/foo.egg-info/.empty" ""))))
|
||||
(wheel (wheel-file "foo-1.0.0"
|
||||
`(("METADATA" ,test-metadata)))))
|
||||
(with-pypi `(("/foo-1.0.0.tar.gz" 200 ,(file-dump tarball))
|
||||
|
@ -362,7 +453,7 @@ (define-syntax-rule (with-pypi responses body ...)
|
|||
(x
|
||||
(pk 'fail x #f))))))
|
||||
|
||||
(test-assert "pypi->guix-package, no usable requirement file."
|
||||
(test-assert "pypi->guix-package, no usable requirement file, no wheel."
|
||||
(let ((tarball (pypi-tarball "foo-1.0.0"
|
||||
'(("foo.egg-info/.empty" "")))))
|
||||
(with-pypi `(("/foo-1.0.0.tar.gz" 200 ,(file-dump tarball))
|
||||
|
|
Loading…
Reference in a new issue