summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--guix/import/pypi.scm74
-rw-r--r--tests/pypi.scm101
2 files changed, 152 insertions, 23 deletions
diff --git a/guix/import/pypi.scm b/guix/import/pypi.scm
index 479b4684a3..1fd3481a09 100644
--- a/guix/import/pypi.scm
+++ b/guix/import/pypi.scm
@@ -57,6 +57,7 @@
#: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 @@ be extracted in a temporary directory."
(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 @@ be extracted in a temporary directory."
(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 '() '()))))
-
- ;; 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)))
+ (list #f #f))))
+
+ (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
diff --git a/tests/pypi.scm b/tests/pypi.scm
index c9aee34d8b..fe00e429b7 100644
--- a/tests/pypi.scm
+++ b/tests/pypi.scm
@@ -112,6 +112,20 @@ Mock
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 @@ files specified by SPECS. Return its file name."
(x
(pk 'fail x #f))))))
+(test-assert "pypi->guix-package, no wheel, no requires.txt, but pyproject.toml"
+ (let ((tarball (pypi-tarball
+ "foo-1.0.0"
+ `(("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, wheels"
+(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/requires.txt"
- "wrong data \
-to make sure we're testing wheels"))))
+ '(("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 @@ to make sure we're testing wheels"))))
(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))